[
  {
    "path": ".gitignore",
    "content": "\n/target/\n\n# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n.idea/\n\n*.iml\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "\n## 项目简介\n\n本项目采用 SpringBoot 框架构建，致力于为 SpringBoot 应用程序的开发提升开发效率及编程体验。\n\n本项目由多个子项目组成，每个子项目聚焦解决一个问题。\n这里先简单介绍下这些子项目：\n* [commons-api2doc](https://github.com/terran4j/commons/tree/master/commons-api2doc):  \n    简称 Api2Doc，是一款 Http API 文档自动化生成工具，\n    它通过反射的方式，读取 Controller 类的信息，\n    然后自动生成易于阅读的在线 API 文档，节省开发者手工编写 API 文档的工作量。\n* [commons-restpack](https://github.com/terran4j/commons/tree/master/commons-restpack):  \n    简称 RestPack，是一款 Http API 数据包装框架，\n    它可以将 Http API 的返回结果包装成统一的报文格式。\n* [commons-dsql](https://github.com/terran4j/commons/tree/master/commons-dsql):  \n    简称 DSQL，是一款从 SQL 到对象的自动映射框架，它尤其擅长动态复杂 SQL 的处理。\n    它结合了现在两大主流持久层框架 JPA 及 MyBatis 的优点，\n    从而更进一步的提高了持久层的开发效率。\n* [commons-hedis](https://github.com/terran4j/commons/tree/master/commons-hedis):  \n    简称 Hedis，是 Happy for using Redis 之意，目标是让 Redis 使用起来更容易。\n    Hedis 集成了 spring-data-redis， Jedis，Redisson 等 Redis 客户端框架，\n    并用它们解决一些具体的问题，如：分布式同步、轻量级分布式定时调度等。 \n    \n\n## 适用用户\n\n适合有 Java / Kotlin + SpringBoot 开发经验的开发者们使用。\n\n如果您有 Java 开发经验但对Spring Boot 还不熟悉的话，建议先阅读笔者写过的一本书\n[《Spring Boot 快速入门》](http://www.jianshu.com/nb/14688855?order_by=seq)。\n这本书的目标是帮助有 Java 开发经验的程序员们快速掌握 Spring Boot 开发技巧，\n感受到 Spring Boot 的极简开发风格及超爽编程体验。\n\n\n## 软件版本\n\n本项目中所用到的基础软件，均基于以下版本构建：\n* Java:  1.8\n* Maven:  3.3.9\n* SpringBoot:  1.5.9.RELEASE\n\n本项目及所有子项目均在以上版本测试过，可以正常运行。\n其它版本理论上相同，应该没啥区别，若遇到问题，欢迎反馈！\n"
  },
  {
    "path": "build.txt",
    "content": "\n## 统一修改版本号\nmvn versions:set \"-DnewVersion=1.0.2-SNAPSHOT\"\nmvn versions:commit\n\n## 当版本为 SNAPSHOT 时的测试：\nmvn clean test\n\n## 当版本为 SNAPSHOT 时的部署：\nmvn clean deploy -DskipTests\n\n## 本地构建打包。\nmvn clean install -DskipTests\n\n## 所有项目一起部署太慢了，容易失败，所有分开部署：\n## 建议在早上 10:00 以后执行（这会美国是晚上，网络稍好点）\nmvn clean deploy -DskipTests -P release -pl commons-util -am     // 03:33\nmvn clean deploy -DskipTests -P release -pl commons-website     // 02:27\nmvn clean deploy -DskipTests -P release -pl commons-restpack    // 02:11\nmvn clean deploy -DskipTests -P release -pl commons-api2doc     // 12:58\nmvn clean deploy -DskipTests -P release -pl commons-hedis         // 03:05\nmvn clean deploy -DskipTests -P release -pl commons-hi               // 03:12\nmvn clean deploy -DskipTests -P release -pl commons-test            // 02:00\nmvn clean deploy -DskipTests -P release -pl commons-dsql           // 03:14"
  },
  {
    "path": "commons-api2doc/.gitignore",
    "content": "/target/\n/.settings/\n/.classpath\n/.project\n*.iml\n"
  },
  {
    "path": "commons-api2doc/README.md",
    "content": "\n本文介绍一个非常好用的自动化生成 Restful API 文档的工具——Api2Doc\n它基于 SpringBoot ，原理类似于 Swagger2，但比 Swagger2 要简单好用。\n\n此项目已经放到 github 中，需要源码的朋友请点击\n[这里](https://github.com/terran4j/commons/tree/master/commons-api2doc)\n\n## 目录\n\n* 项目背景\n* Api2Doc 简介\n* 引入 Api2Doc 依赖\n* 启用 Api2Doc 服务\n* 给 Controller 类上添加文档注解\n* @Api2Doc 注解详述\n* @ApiComment 注解详述\n* @ApiError 注解详述\n* 给文档菜单项排序\n* 补充自定义文档\n* 定制文档的欢迎页\n* 定制文档的标题及图标\n* 关闭 Api2Doc 服务\n* 后续开发计划\n\n## 项目背景\n\n在互联网/移动互联网软件的研发过程中，大多数研发团队前后台分工是非常明确的，\n后台工程师负责服务端系统的开发，一般是提供 HTTP/HTTPS 的 Restful API 接口，\n前端工程师则负责 Android、iOS、H5页面的开发，需要调用 Restful API 接口。\n\n这就需要有一套 Restful API 文档，以帮助两方在 API 接口进行沟通，并达成一致意见。\n一般情况下，编写文档的工作都会落在后台工程师身上，毕竟 API 是他们提供的嘛。\n\n但问题是，编写 Restful API 文档是一件既繁琐、又费时、还对提高技术能力没啥帮助的苦差事，\n尤其在是快速迭代、需求频繁修改的项目中，改了代码还要同步改文档，\n哪点改错了或改漏了都可能产生前后端实现的不一致，导致联调时发现 BUG，\n这个锅最终还是要后台工程师来背（宝宝心里苦啊...）。\n\n因此，业界就出现了一些**根据代码自动生成 Restful API 文档**的开源项目，\n与 Spring Boot 结合比较好的是 Swagger2，Swagger2 通过读取 Controller \n代码中的注解信息，来自动生成 API 文档，可以节省大量的手工编写文档的工作量。\n\n本项目作者之前也是用的 Swagger2，但发现 Swagger2 也有好多地方用得不爽：\n\n第一，**Swagger2 的注解非常臃肿**，我们看下这段代码：\n\n```java\n\n@RestController\n@RequestMapping(value = \"/user3\")\npublic class UserController2Swagger2 {\n\n    @ApiOperation(value = \"获取指定id用户详细信息\",\n            notes = \"根据user的id来获取用户详细信息\",\n            httpMethod = \"GET\")\n    @ApiImplicitParams({\n            @ApiImplicitParam(name = \"userName\", value = \"用户名\",\n                    paramType = \"query\", required = true, dataType = \"String\"),\n            @ApiImplicitParam(name = \"password\", value = \"用户密码\",\n                    paramType = \"query\", required = true, dataType = \"String\")\n    })\n    @RequestMapping(name = \"用户注册\", value = \"/regist\",\n            method = RequestMethod.GET)\n    public UserInfo regist(@RequestParam(\"userName\") String userName,\n                           @RequestParam(\"password\") String password) {\n        return new UserInfo();\n    }\n}\n```\n\n@ApiOperation、@ApiImplicitParam 都是 Swagger2 提供的注解，用于定义 API 信息。\n其实，API 方法本身就包含了很多信息，如HTTP Method、参数名、参数类型等等，\n像 @ApiImplicitParam 中除了 value 属性有用外，其它都是重复描述。\n\n\n第二，Swagger2 的页面排版不太友好，它是一个垂直排列的方式，不利于信息的展示。\n并且看 API 详细信息还要一个个展开，中间还夹杂着测试的功能，反正作为文档是不易于阅读；\n至于作为测试工具嘛...，现在专业的测试工具也有很多，测试人员好像也不选它。\n\n第三，Swagger2 还有好多细节没做好，比如看这个图：\n\n![swgger2-1.png](http://upload-images.jianshu.io/upload_images/4489584-575b3f94d746d921.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n红框中的 API 其实对应的是同一个方法，之所以有这么多，只是因为写这个方法\n时没有指定 method：\n\n```java\n@RestController\n@RequestMapping(value = \"/user2\")\npublic class UserController2Swagger2 {\n    \n    @RequestMapping(value = \"/do_something\")\n    public void doSomethingRequiredLogon() {\n    }\n    \n    // 其它方法，这里省略...\n}\n``` \n\n（当没指定 method 时，Spring Boot 会默认让这个接口支持所有的 method）\n\n因此，考虑到与其长长久久忍受 Swagger2 的各种不爽，不如花些时间做一个\n更好用的“自动化文档系统”，于是就诞生了本项目： Api2Doc 。 \n\n\n## Api2Doc 简介\n\nApi2Doc 专注于 Restful API 文档的自动生成，它的原理与 Swagger2 是类似的，\n都是通过反射，分析 Controller 中的信息生成文档，但它要比 Swagger2 好很多。\n\n最大的不同是： **Api2Doc 比 Swagger2 要少写很多代码**。\n\n举个例子，使用 Swagger2 的代码是这样的：\n\n```java\n\n@RestController\n@RequestMapping(value = \"/user\")\npublic class UserController {\n\n    @ApiOperation(value = \"添加用户\", httpMethod = \"POST\",\n            notes = \"向用户组中添加用户，可以指定用户的类型\")\n    @ApiImplicitParams({\n            @ApiImplicitParam(name = \"group\", value = \"用户组名\",\n                    paramType = \"query\", required = true, dataType = \"String\"),\n            @ApiImplicitParam(name = \"name\", value = \"用户名\",\n                    paramType = \"query\", required = true, dataType = \"String\"),\n            @ApiImplicitParam(name = \"type\", value = \"用户类型\",\n                                paramType = \"query\", required = true, dataType = \"String\")\n    })\n    @RequestMapping(value = \"/addUser\", method = RequestMethod.POST)\n    public User addUser(String group, String name, String type) {\n        return null; // TODO:  还未实现。\n    }\n}\n```\n\n我们看下使用 Api2Doc 注解修饰后的代码：\n\n```java\n@Api2Doc(id = \"users\")\n@ApiComment(seeClass = User.class)\n@RestController\n@RequestMapping(value = \"/api2doc/demo2\")\npublic class UserController2 {\n\n    @ApiComment(\"添加一个新的用户。\")\n    @RequestMapping(name = \"新增用户\",\n            value = \"/user\", method = RequestMethod.POST)\n    public User addUser(String group, String name, String type) {\n        return null; // TODO:  还未实现。\n    }\n    \n    // 其它方法，这里省略...\n}\n```\n\n看，Api2Doc 仅需要在方法上加上 @Api2Doc @ApiComment 注解等极少数代码，\n但它生成的文档可一点不含糊，如下图所示：\n\n![api2doc-2-1.png](http://upload-images.jianshu.io/upload_images/4489584-98f94cb360c0ccde.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n![api2doc-2-2.png](http://upload-images.jianshu.io/upload_images/4489584-fedf2897f5c217b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n有的朋友可能会觉得很奇怪：文档页面上的说明、示例值等内容，在代码中没有写啊，\n这些是哪来的呢？\n\n这里涉及到 Api2Doc 的核心设计理念，就是：它尽可能通过智能分析，自动收集\n生成文档所需的信息，从而**让用户少写代码**。\n\n说得有点抽象哈，下面我们来正面回答这个问题，请大家注意这个类上有一个注解：\n\n```\n@ApiComment(seeClass = User.class)\n```\n\n它意思是： 在 API 方法上遇到没写说明信息时，请参照 User 类中的定义的说明信息。\n\n下面是 User 类的代码：\n\n```java\npublic class User {\n\n    @ApiComment(value = \"用户id\", sample = \"123\")\n    private Long id;\n\n    @ApiComment(value = \"用户名\", sample = \"terran4j\")\n    private String name;\n\n    @ApiComment(value = \"账号密码\", sample = \"sdfi23skvs\")\n    private String password;\n\n    @ApiComment(value = \"用户所在的组\", sample = \"研发组\")\n    private String group;\n\n    @ApiComment(value = \"用户类型\", sample = \"admin\")\n    private UserType type;\n\n    @ApiComment(value = \"是否已删除\", sample = \"true\")\n    @RestPackIgnore\n    private Boolean deleted;\n\n    @ApiComment(value = \"创建时间\\n也是注册时间。\")\n    private Date createTime;\n\n    // 省略  getter / setter 方法。\n}\n```\n\n大家看明白了没？ API 方法中的参数，如果与 User 类的属性同名的话，就用类\n属性的 @ApiComment 说明信息自动填充。\n\n其实这也符合实际的业务逻辑。因为在大部分项目中，有的字段会在多个实体类、\n多个 API 方法中用到，完全没有必要重复编写其说明信息，只要有一个地方定义好了，\n然后其它地方参照就行了。\n\n当然，这只是 Api2Doc 比 Swagger2 好用的特性之一，还有不少比 Swagger2 好用的地方。\n\n下面我们就来全面讲解它的用法，希望可以帮助开发者们从文档编写的苦海中解脱出来。\n\n\n## 引入 Api2Doc 依赖\n\n如果是 maven ，请在 pom.xml 中添加依赖，如下所示：\n\n```xml\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-api2doc</artifactId>\n            <version>${api2doc.version}</version>\n        </dependency>\n```\n\n如果是 gradle，请在 build.gradle 中添加依赖，如下所示：\n\n```groovy\ncompile \"com.github.terran4j:terran4j-commons-api2doc:${api2doc.version}\"\n```\n\n${api2doc.version} **最新稳定版，请参考 [这里](https://github.com/terran4j/commons/blob/master/version.md)**\n\n\n## 启用 Api2Doc 服务\n\n本教程的示例代码在 src/test/java 目录的 com.terran4j.demo.api2doc 中，\n您也可以从 [这里](https://github.com/terran4j/commons/tree/master/commons-api2doc/src/test/java/com/terran4j/demo/api2doc) 获取到。\n\n首先，我们需要在有 @SpringBootApplication 注解的类上，添加 @EnableApi2Doc \n注解，以启用 Api2Doc 服务，如下代码所示：\n\n```java\npackage com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.config.EnableApi2Doc;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n//  文档访问地址： http://localhost:8080/api2doc/home.html\n@EnableApi2Doc\n@SpringBootApplication\npublic class Api2DocDemoApp {\n\n    public static void main(String[] args) {\n        SpringApplication.run(Api2DocDemoApp.class, args);\n    }\n\n}\n``` \n\n## 给 Controller 类上添加文档注解\n\n然后我们在 RestController 类添加 @Api2Doc 注解，在需要有文档说明的地方\n添加 @ApiComment 注解即可，如下所示：\n\n```java\npackage com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\n@Api2Doc(id = \"demo1\", name = \"用户接口1\")\n@ApiComment(seeClass = User.class)\n@RestController\n@RequestMapping(value = \"/api2doc/demo1\")\npublic class UserController1 {\n\n    @ApiComment(\"添加一个新的用户。\")\n    @RequestMapping(name = \"新增用户\",\n            value = \"/user\", method = RequestMethod.POST)\n    public User addUser(String group, String name,\n                        @ApiComment(\"用户类型\") UserType type) {\n        return null; // TODO:  还未实现。\n    }\n}\n```\n\n这个方法的返回类型 User 类的定义为：\n\n```java\npublic class User {\n\n    @ApiComment(value = \"用户id\", sample = \"123\")\n    private Long id;\n\n    @ApiComment(value = \"用户名\", sample = \"terran4j\")\n    private String name;\n\n    @ApiComment(value = \"账号密码\", sample = \"sdfi23skvs\")\n    private String password;\n\n    @ApiComment(value = \"用户所在的组\", sample = \"研发组\")\n    private String group;\n\n    @ApiComment(value = \"用户类型\", sample = \"admin\")\n    private UserType type;\n\n    @ApiComment(value = \"是否已删除\", sample = \"true\")\n    @RestPackIgnore\n    private Boolean deleted;\n\n    @ApiComment(value = \"创建时间\\n也是注册时间。\")\n    private Date createTime;\n\n    // 省略  getter / setter 方法。\n}\n```\n\n以及 type 属性的类型，也就是 UserType 类的定义为：\n\n```java\npackage com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\n\npublic enum UserType {\n\n    @ApiComment(\"管理员\")\n    admin,\n\n    @ApiComment(\"普通用户\")\n    user\n}\n```\n\n编写好代码后，我们运行 main 函数，访问 Api2Doc 的主页面：\n\n```\nhttp://localhost:8080/api2doc/home.html\n```\n\n文档页面如下：\n\n![api2doc-3-1.png](http://upload-images.jianshu.io/upload_images/4489584-ef8f5a56917da47f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n![api2doc-3-2.png](http://upload-images.jianshu.io/upload_images/4489584-55ea3a295a009855.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n说明 Api2Doc 服务起作用了，就是这么简单！\n\n\n## @Api2Doc 注解详述\n\nApi2Doc 一共有 3 个注解：@Api2Doc、@ApiComment 及 @ApiError 。\n\n@Api2Doc 用于对文档的生成进行控制。\n\n@Api2Doc 修饰在类上，表示这个类会参与到文档生成过程中，Api2Doc 服务\n会扫描 Spring 容器中所有的 Controller 类，只有类上有 @Api2Doc 的类，\n才会被生成文档，一个类对应于文档页面左侧的一级菜单项，@Api2Doc 的 \nname 属性则表示这个菜单项的名称。\n\n@Api2Doc 也可以修饰在方法，不过在方法上的  @Api2Doc 通常是可以省略，\n Api2Doc 服务会扫描这个类的所有带有 @RequestMapping 的方法，\n 每个这样的方法对应文档页面的左侧的二级菜单项， 菜单项的名称取 \n @RequestMapping 的 name 属性，当然您仍然可以在方法上用  @Api2Doc \n 的 name 属性进行重定义。\n \n \n ## @ApiComment 注解详述\n \n @ApiComment 用于对 API 进行说明，它可以修饰在很多地方：\n * 修饰在类上，表示对这组 API 接口进行说明；\n * 修饰在方法上，表示对这个 API 接口进行说明；\n * 修饰在参数上，表示对这个 API 接口的请求参数进行说明；\n * 修饰在返回类型的属性上，表示对这个 API 接口的返回字段进行说明；\n * 修饰在枚举项上，表示对枚举项进行说明；\n\n如果相同名称、相同意义的属性或参数字段，其说明已经在别的地方定义过了，\n可以用 @ApiComment 的 seeClass 属性表示采用指定类的同名字段上的说明信息，\n所以如这段代码：\n\n```java\n@Api2Doc(id = \"demo1\", name = \"用户接口1\")\n@ApiComment(seeClass = User.class)\n@RestController\n@RequestMapping(value = \"/api2doc/demo1\")\npublic class UserController1 {\n\n    @ApiComment(\"添加一个新的用户。\")\n    @RequestMapping(name = \"新增用户\",\n            value = \"/user\", method = RequestMethod.POST)\n    public User addUser(String group, String name, UserType type) {\n        return null; // TODO:  还未实现。\n    }\n}\n```\n\n虽然 group, name ,type 三个参数没有用 @ApiComment 进行说明，\n但由于这个类上有 @ApiComment(seeClass = User.class) ，\n因此只要 User 类中有 group, name ,type 字段并且有  @ApiComment 的说明就行了。\n\n\n## @ApiError 注解详述\n\n@ApiError 用于定义错误码，有的 API 方法在执行业务逻辑时会产生错误，\n出错后会在返回报文包含错误码，以方便客户端根据错误码作进一步的处理，\n因此也需要在 API 文档上体现错误码的说明。\n\n如下代码演示了 @ApiError 的用法：\n\n```java\n@Api2Doc(id = \"demo\", name = \"用户接口\", order = 0)\n@ApiComment(seeClass = User.class)\n@RestController\n@RequestMapping(value = \"/src/test/resources/demo\")\npublic class UserController {\n    \n    @Api2Doc(order = 50)\n    @ApiComment(\"根据用户id，删除指定的用户\")\n    @ApiError(value = \"user.not.found\", comment = \"此用户不存在！\")\n    @ApiError(value = \"admin.cant.delete\", comment = \"不允许删除管理员用户！\")\n    @RequestMapping(name = \"删除指定用户\",\n            value = \"/user/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) {\n    }\n}\n```\n\n@ApiError 的 value 属性表示错误码，comment 表示错误码的说明。\n\n错误码信息会显示在文档的最后面，效果如下所示：\n\n![api2doc-7.png](http://upload-images.jianshu.io/upload_images/4489584-44a77bbb3c1e84da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n## 给文档菜单项排序\n\n我们可以用 @Api2Doc 中的 order 属性给菜单项排序，order 的值越小，\n该菜单项就越排在前面，比如对于这段代码：\n\n```java\npackage com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\n\n@Api2Doc(id = \"demo2\", name = \"用户接口2\", order = 1)\n@ApiComment(seeClass = User.class)\n@RestController\n@RequestMapping(value = \"/api2doc/demo2\")\npublic class UserController2 {\n\n    @Api2Doc(order = 10)\n    @ApiComment(\"添加一个新的用户。\")\n    @RequestMapping(name = \"新增用户\",\n            value = \"/user\", method = RequestMethod.POST)\n    public User addUser(\n            @ApiComment(\"用户组名称\") String group,\n            @ApiComment(\"用户名称\") String name,\n            @ApiComment(\"用户类型\") UserType type) {\n        return null; // TODO:  还未实现。\n    }\n\n    @Api2Doc(order = 20)\n    @ApiComment(\"根据用户id，查询此用户的信息\")\n    @RequestMapping(name = \"查询单个用户\",\n            value = \"/user/{id}\", method = RequestMethod.GET)\n    public User getUser(@PathVariable(\"id\") Long id) {\n        return null; // TODO:  还未实现。\n    }\n\n    @Api2Doc(order = 30)\n    @ApiComment(\"查询所有用户，按注册时间进行排序。\")\n    @RequestMapping(name = \"查询用户列表\",\n            value = \"/users\", method = RequestMethod.GET)\n    public List<User> getUsers() {\n        return null; // TODO:  还未实现。\n    }\n\n    @Api2Doc(order = 40)\n    @ApiComment(\"根据指定的组名称，查询该组中的所有用户信息。\")\n    @RequestMapping(name = \"查询用户组\",\n            value = \"/group/{group}\", method = RequestMethod.GET)\n    public UserGroup getGroup(@PathVariable(\"group\") String group) {\n        return null; // TODO:  还未实现。\n    }\n}\n```\n\n显示的结果为：\n\n![api2doc-3.png](http://upload-images.jianshu.io/upload_images/4489584-0818fdef543c8c07.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n在类上的 @Api2Doc 同样可以给一级菜单排序，规则是一样的，这里就不演示了。\n\n\n## 补充自定义文档\n\n有时候光有自动生成的 API 文档似乎还不太完美，或许我们想补充点别的什么东西，\n比如： 对项目的背景介绍、技术架构说明之类，那这个要怎么弄呢？\n\nApi2Doc 允许用 md 语法手工编写文档，并集成到自动生成的 API 文档之中，方法如下：\n\n首先，要在类上的 @Api2Doc 定义 id 属性，比如对下面这个类：\n\n```java\npackage com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@Api2Doc(id = \"demo3\", name = \"用户接口3\")\n@RestController\n@RequestMapping(value = \"/api2doc/demo3\")\npublic class UserController3 {\n\n    @Api2Doc(order = 10)\n    @RequestMapping(name = \"接口1\", value = \"/m1\")\n    public void m1() {\n    }\n\n    @Api2Doc(order = 20)\n    @RequestMapping(name = \"接口2\", value = \"/m2\")\n    public void m2() {\n    }\n}\n``` \n\n@Api2Doc(id = \"demo3\", name = \"用户接口3\") 表示：对应的一级菜单“用户接口3”\n的 id 为 demo3。\n\n然后，我们在 src/main/resources 中创建目录  api2doc/demo3，\n前面的 api2doc 是固定的，后面的 demo3 表示这个目录中的文档是添加到\nid 为 demo3 的一级文档菜单下。\n\n然后我们在 api2doc/demo3 目录中编写 md 格式的文档，如下图所示：\n\n![api2doc-4.png](http://upload-images.jianshu.io/upload_images/4489584-a76a84061f2771d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n文件名的格式为 ${order}-${文档名称}.md，即 - 号前面的数字表示这个文档的排序，\n与 @Api2Doc 中的 order 属性是一样的，而 - 号后面是文档名称，也就是二级菜单的名称。\n\n因此，最后文档的显示效果为：\n\n![api2doc-5.png](http://upload-images.jianshu.io/upload_images/4489584-73814ce5bde91b2d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n看，手工编写的补充文档与自动生成的 API 文档，通过 order 进行排序组合在一起，\n看起来毫无违和感。\n\n\n## 定制文档的欢迎页\n\n每次访问文档页面 http://localhost:8080/api2doc/home.html 时，\n中间的内容是非常简单的一句：\n\n```\n欢迎使用 Api2Doc ！\n```\n\n这似乎有点不太好，不过没关系，我们可以编写自己的欢迎页。\n\n方法很简单，在 src/main/resources 目录的 api2doc 目录下，创建一个名为 \nwelcome.md 的文件（这个名称是固定的），然后用 md 语法编写内容就可以。\n\n\n## 配置文档的标题及图标\n\n可以在 application.yml 中配置文档的标题及图标，如下所示：\n\n```yaml\napi2doc:\n  title: Api2Doc示例项目——接口文档\n  icon: https://spring.io/img/homepage/icon-spring-framework.svg\n```\n\n图标为一个全路径 URL，或本站点相对路径 URL 都行。\n\n配置后的显示效果为： \n\n![api2doc-6.png](http://upload-images.jianshu.io/upload_images/4489584-494a0c8042aaffb3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n## 关闭 Api2Doc 服务 \n\n您在 application.yml 中配置 api2doc.enabled 属性，以开启或关闭 Api2Doc 服务，如：\n\n```yaml\n# 本地环境\napi2doc:\n  title: Api2Doc示例项目——接口文档\n  icon: https://spring.io/img/homepage/icon-spring-framework.svg\n\n---\n# 线上环境\nspring:\n  profiles: online\n\napi2doc:\n  enabled: false\n```\n\napi2doc.enabled 为 false 表示关闭 Api2Doc 服务，不写或为 true 表示启用。\n\n由于  Api2Doc 服务没有访问权限校验，建议您在受信任的网络环境（如公司内网）\n中才启用 Api2Doc 服务。\n\n\n## 后续开发计划\n\n参见[后续开发计划](https://github.com/terran4j/commons/blob/master/commons-api2doc/doc/TODO.md)\n"
  },
  {
    "path": "commons-api2doc/doc/TODO.md",
    "content": "\n## 后续开发计划\n\nApi2Doc 项目后续计划要开发的功能如下：\n\n* 参数为对象，参数类型object，可以指出具体类型\n* 可以导出为 html 格式的文档文件，可以离线浏览，\n   支持在页面上手工操作导出，以及程序调用 API 自定义导出两种方式。\n* 可以导出为 md 格式的文档文件，可以放到 md 运行环境中浏览，\n   支持在页面上手工操作导出，以及程序调用 API 自定义导出两种方式。\n* 对测试进行支持。\n* 文档样式设计得更漂亮。\n* 文档样式支持自定义。\n\n### 1.0.2 （已发布于 2018-04-06 ）\n\n新增了以下功能：\n1.  支持各种用 @XxxMapping 修饰的方法生成文档，包括：\n         `@GetMapping`、\n         `@PostMapping`、\n         `@PutMapping`、\n         `@DeleteMapping`、\n         `@PatchMapping`；\n     （之前只支持 `@RequestMapping` 。）\n2. 支持各种形式的参数，包括：\n         `@PathVariable`、\n         `@RequestHeader`、\n         `@CookieValue`、\n         `@RequestPart`；\n     （之前只支持 `@RequestParam` 。）\n     并在文档页面的“请求参数”表格，加上“参数形式”这一列。\n3.  文档页面，“URL示例”改为“请求示例”，\n     请求示例为 curl 命令格式，并支持所有的 HTTP 方法 ，\n    （之前是 URL 格式，并且只支持了 GET 方法）。\n    \n修复了以下 BUG:\n\n1. 修复当返回类型为简单类型时未能显示在文档的 BUG。\n\n"
  },
  {
    "path": "commons-api2doc/doc/aboutCurl.md",
    "content": "\n## 什么是 curl\ncurl 是一款很强大的 http 命令行工具，可以的向服务端发起 HTTP/HTTPS 请求，\n是一个很方便 HTTP API 测试手段。\n\n## 如何安装 curl \n - 如果是 Linux / MAC 系统，一般系统都自带了 curl （如果没有安装也很简单，请自行百度）。\n - 如果是 Windows 系统，建议先安装 Cygwin64 ，然后在 Cygwin64 中安装使用 curl 。\n \n下面就介绍下如何使用 Cygwin64 包管理器安装 curl\n\nCygwin64 包管理器下载地址：\n[http://www.cygwin.com/setup-x86_64.exe](http://www.cygwin.com/setup-x86_64.exe)\n\n下载后运行，一直下一步到以下界面：\n\n![image](http://upload-images.jianshu.io/upload_images/4489584-b4431f26c3440bd6?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\nView 选择 Full，Search 后面输入：curl\n\n然后点击第一行的Skip，点击下一步，后面就按默认的设置一步步安装即可。"
  },
  {
    "path": "commons-api2doc/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<parent>\n\t\t<groupId>com.github.terran4j</groupId>\n\t\t<artifactId>terran4j-commons-parent</artifactId>\n\t\t<version>1.0.4-SNAPSHOT</version>\n\t</parent>\n\n\t<artifactId>terran4j-commons-api2doc</artifactId>\n\t<packaging>jar</packaging>\n\t<name>terran4j-commons-api2doc</name>\n\t<url>https://github.com/terran4j/commons</url>\n\n\t<dependencies>\n\n        <!-- md 转 html 工具 -->\n\t\t<dependency>\n\t\t    <groupId>com.vladsch.flexmark</groupId>\n\t\t    <artifactId>flexmark</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t    <groupId>com.vladsch.flexmark</groupId>\n\t\t    <artifactId>flexmark-util</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t    <groupId>com.vladsch.flexmark</groupId>\n\t\t    <artifactId>flexmark-ext-tables</artifactId>\n\t\t</dependency>\n        <dependency>\n            <groupId>com.vladsch.flexmark</groupId>\n            <artifactId>flexmark-ext-spec-example</artifactId>\n        </dependency>\n\n        <!-- 模板引擎 -->\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-freemarker</artifactId>\n\t\t</dependency>\n\n\t\t<!-- terran4j 工具类库。 -->\n\t\t<dependency>\n\t\t\t<groupId>com.github.terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-util</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.github.terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-website</artifactId>\n\t\t</dependency>\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-restpack</artifactId>\n        </dependency>\n\n\t\t<!-- aspectj -->\n\t\t<dependency>\n\t\t\t<groupId>org.aspectj</groupId>\n\t\t\t<artifactId>aspectjweaver</artifactId>\n\t\t</dependency>\n\n\t\t<!-- json -->\n\t\t<dependency>\n\t\t\t<groupId>com.google.code.gson</groupId>\n\t\t\t<artifactId>gson</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-core</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-annotations</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.module</groupId>\n\t\t\t<artifactId>jackson-module-jsonSchema</artifactId>\n\t\t</dependency>\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/Api2DocMocker.java",
    "content": "package com.terran4j.commons.api2doc;\n\nimport com.terran4j.commons.api2doc.impl.Api2DocObjectFactory;\n\nimport java.util.List;\n\npublic class Api2DocMocker {\n\n    public static <T> T mockBean(Class<T> clazz) {\n        return Api2DocObjectFactory.createBean(clazz);\n    }\n\n    public static <T> List<T> mockList(Class<T> clazz, int size) {\n        return Api2DocObjectFactory.createList(clazz, size);\n    }\n\n    public static <T> T[] mockArray(Class<T> clazz, int size) {\n        return Api2DocObjectFactory.createArray(clazz, size);\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/annotations/Api2Doc.java",
    "content": "package com.terran4j.commons.api2doc.annotations;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.springframework.core.annotation.AliasFor;\n\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })\npublic @interface Api2Doc {\n\t\n\tint DEFAULT_ORDER = 100;\n\n\t/**\n\t * 文档的id。<br>\n\t * 对于一个类中的重载方法，一定要用不同的 id 区分，不然会出错。\n\t * \n\t * @return\n\t */\n\t@AliasFor(\"value\")\n\tString id() default \"\";\n\n\t/**\n\t * 文档的id。<br>\n\t * 对于一个类中的重载方法，一定要用不同的 id 区分，不然会出错。\n\t * \n\t * @return\n\t */\n\t@AliasFor(\"id\")\n\tString value() default \"\";\n\n\t/**\n\t * 是否忽略此文档。\n\t * \n\t * @return\n\t */\n\tboolean ignore() default false;\n\n\t/**\n\t * 设置文档的排序。<br>\n\t * 数字越小，排序越靠前。\n\t * \n\t * @return\n\t */\n\tint order() default DEFAULT_ORDER;\n\t\n\tString name() default \"\";\n\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/annotations/ApiComment.java",
    "content": "package com.terran4j.commons.api2doc.annotations;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({\n\t\tElementType.TYPE,\n\t\tElementType.METHOD,\n\t\tElementType.PARAMETER,\n\t\tElementType.FIELD,\n})\npublic @interface ApiComment {\n\n\tString value() default \"\";\n\n\tString sample() default \"\";\n\n\tClass<?> seeClass() default Object.class;\n\n\tString seeField() default \"\";\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/annotations/ApiError.java",
    "content": "package com.terran4j.commons.api2doc.annotations;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Repeatable;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD })\n@Repeatable(ApiErrors.class)\npublic @interface ApiError {\n\n\tString value();\n\t\n\tString comment() default \"\";\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/annotations/ApiErrors.java",
    "content": "package com.terran4j.commons.api2doc.annotations;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Documented\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ ElementType.METHOD })\npublic @interface ApiErrors {\n\n\tApiError[] value();\n\t\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/CodeConfig.java",
    "content": "package com.terran4j.commons.api2doc.codewriter;\n\nimport java.util.List;\n\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\n\npublic class CodeConfig {\n\n\tpublic List<ApiParamObject> getExtraPrams(ApiDocObject doc) {\n\t\treturn null;\n\t}\n\n\tprivate String pkgName;\n\n\tprivate String declaredComment;\n\n\tpublic String getPkgName() {\n\t\treturn pkgName;\n\t}\n\n\tpublic void setPkgName(String pkgName) {\n\t\tthis.pkgName = pkgName;\n\t}\n\n\tpublic String getDeclaredComment() {\n\t\treturn declaredComment;\n\t}\n\n\tpublic void setDeclaredComment(String declareComment) {\n\t\tthis.declaredComment = declareComment;\n\t}\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/CodeOutput.java",
    "content": "package com.terran4j.commons.api2doc.codewriter;\n\npublic interface CodeOutput {\n\n\tvoid writeCodeFile(String fileName, String fileContent);\n\t\n\tvoid setPercent(int percent);\n\t\n\tvoid log(String log, String... args);\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/CodeUtils.java",
    "content": "package com.terran4j.commons.api2doc.codewriter;\n\nimport java.util.Set;\n\npublic class CodeUtils {\n\n\tpublic static final void addImport(Class<?> clazz, Set<String> imports) {\n\t\tif (clazz == null || clazz.getPackage() == null) {\n\t\t\treturn;\n\t\t}\n\n\t\tString paramPkgName = clazz.getPackage().getName();\n\n\t\tif (paramPkgName.equals(\"java.lang\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (paramPkgName.startsWith(\"java.\") || paramPkgName.startsWith(\"javax.\")) {\n\t\t\timports.add(clazz.getName());\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/EnumCodeWriter.java",
    "content": "package com.terran4j.commons.api2doc.codewriter;\n\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.annotation.PostConstruct;\n\nimport com.terran4j.commons.api2doc.impl.Api2DocUtils;\nimport com.terran4j.commons.api2doc.impl.ApiCommentUtils;\nimport com.terran4j.commons.api2doc.impl.FlexibleString;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.impl.ClasspathFreeMarker;\n\nimport freemarker.template.Template;\n\n@Service\npublic class EnumCodeWriter {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(EnumCodeWriter.class);\n\n\t@Autowired\n\tprivate ClasspathFreeMarker classpathFreeMarker;\n\n\tprivate Template enumTemplate = null;\n\n\t@PostConstruct\n\tpublic void init() {\n\t\ttry {\n\t\t\tenumTemplate = classpathFreeMarker.getTemplate(getClass(), //\n\t\t\t\t\t\"enum.java.ftl\");\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic void writeCode(Class<?> currentClass, String className, //\n\t\t\tCodeOutput out, CodeConfig config) throws Exception {\n\t\t\n\t\tif (currentClass == null || !currentClass.isEnum()) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tMap<String, Object> model = new HashMap<>();\n\t\t\n\t\tmodel.put(\"class\", className);\n\n\t\tif (config == null) {\n\t\t\tconfig = new CodeConfig();\n\t\t}\n\t\tmodel.put(\"config\", config);\n\t\t\n\t\tList<EnumInfo> enumInfos = new ArrayList<>();\n\t\tClass<Enum<?>> enumClass = (Class<Enum<?>>) currentClass;\n\t\tEnum[] enums = enumClass.getEnumConstants();\n\t\tfor (Enum enumObject : enums) {\n\t\t\t\n\t\t\tEnumInfo enumInfo = new EnumInfo();\n\t\t\t\n\t\t\tString name = enumObject.name();\n\t\t\tenumInfo.setName(name);\n\t\t\t\n\t\t\tString comment = null;\n\t\t\tField field = null;\n\t\t\ttry {\n\t\t\t\tfield = enumClass.getDeclaredField(name);\n\t\t\t} catch (NoSuchFieldException | SecurityException e1) {\n\t\t\t\tlog.error(\"Can't get field \\\"\" + name + \"\\\" from Enum: \" //\n\t\t\t\t\t\t+ enumClass.getName(), e1);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tApiComment apiComment = field.getAnnotation(ApiComment.class);\n\t\t\tcomment = ApiCommentUtils.getComment(\n\t\t\t\t\tapiComment, null, field.getName());\n\t\t\tif (comment != null) {\n                comment = new FlexibleString(comment).javadoc(1);\n            }\n//\t\t\tif (apiComment != null && StringUtils.hasText(apiComment.value())) {\n//                comment = new FlexibleString(apiComment.value().trim()).javadoc(1);\n//\t\t\t}\n\t\t\tenumInfo.setComment(comment);\n\t\t\t\n\t\t\tenumInfos.add(enumInfo);\n\t\t}\n\t\tmodel.put(\"enums\", enumInfos);\n\t\t\n\t\tString code = classpathFreeMarker.build(enumTemplate, model);\n\t\tout.writeCodeFile(className + \".java\", code);\n\t}\n\t\n\tpublic static final class EnumInfo {\n\t\t\n\t\tprivate String comment;\n\t\t\n\t\tprivate String name;\n\n\t\tpublic String getComment() {\n\t\t\treturn comment;\n\t\t}\n\n\t\tpublic void setComment(String comment) {\n\t\t\tthis.comment = comment;\n\t\t}\n\n\t\tpublic String getName() {\n\t\t\treturn name;\n\t\t}\n\n\t\tpublic void setName(String name) {\n\t\t\tthis.name = name;\n\t\t}\n\t\t\n\t}\n}\n\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/FileCodeOutput.java",
    "content": "package com.terran4j.commons.api2doc.codewriter;\n\nimport java.io.File;\n\nimport com.terran4j.commons.util.Files;\n\npublic class FileCodeOutput implements CodeOutput {\n\t\n\tprivate final String path;\n\t\n\tpublic FileCodeOutput(String path) {\n\t\tsuper();\n\t\tthis.path = path;\n\t}\n\n\t@Override\n\tpublic void writeCodeFile(String fileName, String fileContent) {\n\t\tFile file = new File(path + \"/\" + fileName);\n\t\tFiles.writeFile(fileContent, file);\n\t}\n\n\t@Override\n\tpublic void setPercent(int percent) {\n\t}\n\n\t@Override\n\tpublic void log(String log, String... args) {\n\t}\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/JavaBeanCodeWriter.java",
    "content": "package com.terran4j.commons.api2doc.codewriter;\n\nimport com.terran4j.commons.api2doc.domain.ApiDataType;\nimport com.terran4j.commons.api2doc.domain.ApiResultObject;\nimport com.terran4j.commons.api2doc.domain.DateConverter;\nimport com.terran4j.commons.api2doc.impl.ClasspathFreeMarker;\nimport freemarker.template.Template;\nimport org.apache.commons.collections4.map.HashedMap;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\n\nimport javax.annotation.PostConstruct;\nimport java.util.*;\n\nimport static java.util.Locale.ENGLISH;\n\n@Service\npublic class JavaBeanCodeWriter {\n\n    private static final String GET_PREFIX = \"get\";\n\n    private static final String SET_PREFIX = \"set\";\n\n    private static final String IS_PREFIX = \"is\";\n\n    @Autowired\n    private ClasspathFreeMarker classpathFreeMarker;\n\n    private Template javaBeanTemplate = null;\n\n    @PostConstruct\n    public void init() {\n        try {\n            javaBeanTemplate = classpathFreeMarker.getTemplate(getClass(), //\n                    \"bean.java.ftl\");\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void writeCode(ApiResultObject result, String className, //\n                          CodeOutput out, CodeConfig config) throws Exception {\n\n        List<ApiResultObject> children = result.getChildren();\n        if (children == null || children.size() == 0) {\n            return;\n        }\n\n        Map<String, Object> model = getModel(result, className, config);\n\n        String code = classpathFreeMarker.build(javaBeanTemplate, model);\n\n        out.writeCodeFile(className + \".java\", code);\n    }\n\n    public Map<String, Object> getModel(ApiResultObject result, String className, //\n                                        CodeConfig config) {\n\n        Map<String, Object> model = new HashedMap<>();\n\n        model.put(\"class\", className);\n\n        if (config == null) {\n            config = new CodeConfig();\n        }\n        model.put(\"config\", config);\n\n        // java 类上的注释。\n        String comment = result.getComment().javadoc(0);\n        if (StringUtils.hasText(comment)) {\n            model.put(\"comment\", comment);\n        }\n\n        Set<String> imports = new HashSet<>();\n        model.put(\"imports\", imports);\n\n        List<FieldInfo> fields = new ArrayList<FieldInfo>();\n        List<ApiResultObject> children = result.getChildren();\n        for (ApiResultObject child : children) {\n            FieldInfo field = new FieldInfo();\n\n            String name = child.getId();\n            field.setName(name);\n\n            String type = toTypeName(child);\n            field.setType(type);\n\n            String fieldComment = child.getComment().javadoc(1);\n            if (StringUtils.hasText(fieldComment)) {\n                field.setComment(fieldComment);\n            }\n\n            // Date 自动转成 Long 类型了。\n            Class<?> sourceType = getSourceType(child);\n            CodeUtils.addImport(sourceType, imports);\n\n            boolean isBooleanClass = (child.getDataType() == ApiDataType.BOOLEAN);\n            String getMethod = toGetMethodName(isBooleanClass, name);\n            field.setGetMethod(getMethod);\n\n            String setMethod = toSetMethodName(name);\n            field.setSetMethod(setMethod);\n\n            fields.add(field);\n        }\n\n        model.put(\"fields\", fields);\n\n        return model;\n    }\n\n    private String toGetMethodName(boolean isBooleanClass, String fieldName) {\n        String baseName = getBaseName(fieldName);\n        String methodName = null;\n        if (isBooleanClass) {\n            methodName = IS_PREFIX + baseName;\n        } else {\n            methodName = GET_PREFIX + baseName;\n        }\n        return methodName;\n    }\n\n    private String toSetMethodName(String fieldName) {\n        String baseName = getBaseName(fieldName);\n        String methodName = SET_PREFIX + baseName;\n        return methodName;\n    }\n\n    private String getBaseName(String name) {\n        return name.substring(0, 1).toUpperCase(ENGLISH) + name.substring(1);\n    }\n\n    private Class<?> getSourceType(ApiResultObject result) {\n        Class<?> sourceType = result.getSourceType();\n        return DateConverter.dateAsLongClass(sourceType);\n    }\n\n    private String toTypeName(ApiResultObject result) {\n        ApiDataType dataType = result.getDataType();\n        Class<?> sourceType = getSourceType(result);\n        ;\n        String typeName = sourceType.getSimpleName();\n        if (dataType.isArrayType()) {\n            typeName = typeName + \"[]\";\n        }\n        return typeName;\n    }\n\n    public static final class FieldInfo {\n\n        private String type;\n\n        private String name;\n\n        private String comment;\n\n        private String getMethod;\n\n        private String setMethod;\n\n        public String getType() {\n            return type;\n        }\n\n        public void setType(String type) {\n            this.type = type;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n        public String getComment() {\n            return comment;\n        }\n\n        public void setComment(String comment) {\n            this.comment = comment;\n        }\n\n        public String getGetMethod() {\n            return getMethod;\n        }\n\n        public void setGetMethod(String getMethod) {\n            this.getMethod = getMethod;\n        }\n\n        public String getSetMethod() {\n            return setMethod;\n        }\n\n        public void setSetMethod(String setMethod) {\n            this.setMethod = setMethod;\n        }\n\n    }\n}\n\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/MemoryCodeOutput.java",
    "content": "package com.terran4j.commons.api2doc.codewriter;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class MemoryCodeOutput implements CodeOutput {\n\n\tprivate final Map<String, String> codes = new ConcurrentHashMap<>();\n\n\t@Override\n\tpublic void writeCodeFile(String fileName, String fileContent) {\n\t\tcodes.put(fileName, fileContent);\n\t}\n\n\t@Override\n\tpublic void setPercent(int percent) {\n\t}\n\n\t@Override\n\tpublic void log(String log, String... args) {\n\t}\n\t\n\tpublic String getCode(String fileName) {\n\t\treturn codes.get(fileName);\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/codewriter/RetrofitCodeWriter.java",
    "content": "package com.terran4j.commons.api2doc.codewriter;\n\nimport com.terran4j.commons.api2doc.domain.*;\nimport com.terran4j.commons.api2doc.impl.ClasspathFreeMarker;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.value.KeyedList;\nimport freemarker.template.Template;\nimport freemarker.template.TemplateException;\nimport org.apache.commons.collections4.map.HashedMap;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport javax.annotation.PostConstruct;\nimport java.io.IOException;\nimport java.util.*;\n\n@Service\npublic class RetrofitCodeWriter {\n\n    private static final Logger log = LoggerFactory.getLogger(RetrofitCodeWriter.class);\n\n    @Autowired\n    private JavaBeanCodeWriter javaBeanCodeWriter;\n\n    @Autowired\n    private EnumCodeWriter enumCodeWriter;\n\n    @Autowired\n    private ClasspathFreeMarker classpathFreeMarker;\n\n    private Template interfaceTemplate = null;\n\n    @PostConstruct\n    public void init() {\n        try {\n            interfaceTemplate = classpathFreeMarker.getTemplate(getClass(), //\n                    \"retrofit.java.ftl\");\n            log.info(\"RetrofitCodeWriter inited done.\");\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void writeCode(List<ApiFolderObject> folders, CodeOutput out, CodeConfig config) {\n        if (folders == null || folders.size() == 0) {\n            return;\n        }\n\n        if (out == null) {\n            throw new NullPointerException(\"CodeWriter writer is null.\");\n        }\n\n        Set<Class<?>> enumClasses = new HashSet<>();\n        KeyedList<String, ApiResultObject> javaBeans = new KeyedList<String, ApiResultObject>();\n        for (ApiFolderObject folder : folders) {\n            Map<String, Object> model = toModel(folder, config, javaBeans, enumClasses);\n            String className = toRetrofitClassName(folder.getId());\n            String fileName = className + \".java\";\n            try {\n                String fileContent = classpathFreeMarker.build(interfaceTemplate, model);\n                out.writeCodeFile(fileName, fileContent);\n            } catch (IOException | TemplateException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        Set<Class<?>> writtenClasses = new HashSet<>();\n        List<ApiResultObject> results = javaBeans.getAll();\n        for (ApiResultObject result : results) {\n            Class<?> clazz = result.getSourceType();\n            if (writtenClasses.contains(clazz)) {\n                continue;\n            }\n\n            String className = clazz.getSimpleName();\n            try {\n                javaBeanCodeWriter.writeCode(result, className, out, config);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n\n            writtenClasses.add(clazz);\n        }\n\n        for (Class<?> currentClass : enumClasses) {\n            String className = currentClass.getSimpleName();\n            try {\n                enumCodeWriter.writeCode(currentClass, className, out, config);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n\n    private String toRetrofitClassName(String id) {\n        String name = id.substring(0, 1).toUpperCase(Locale.ENGLISH) + id.substring(1);\n        if (name.endsWith(\"Controller\")) {\n            name = name.replaceAll(\"Controller\", \"Retrofit\");\n        } else {\n            name += \"Retrofit\";\n        }\n        return name;\n    }\n\n    private Map<String, Object> toModel(ApiFolderObject folder, CodeConfig config, //\n                                        KeyedList<String, ApiResultObject> javaBeans, Set<Class<?>> enumClasses) {\n\n        Map<String, Object> model = new HashedMap<>();\n\n        if (config == null) {\n            config = new CodeConfig();\n        }\n        model.put(\"config\", config);\n\n        String className = toRetrofitClassName(folder.getId());\n        model.put(\"class\", className);\n\n        String comment = folder.getComment().javadoc(0);\n        if (StringUtils.hasText(comment)) {\n            model.put(\"comment\", comment);\n        }\n\n        Set<String> imports = new HashSet<>();\n        model.put(\"imports\", imports);\n\n        List<MethodInfo> methods = new ArrayList<>();\n        List<ApiDocObject> docs = folder.getDocs();\n        if (docs != null) {\n            for (ApiDocObject doc : docs) {\n                MethodInfo method = toMethodInfo(doc, config, imports);\n                methods.add(method);\n\n                List<ApiResultObject> results = doc.getResults();\n                if (results == null || results.size() == 0) {\n                    continue;\n                }\n\n                for (ApiResultObject result : results) {\n                    String groupId = result.getGroupId();\n                    if (StringUtils.hasText(groupId)\n                            && !javaBeans.containsKey(groupId)) {\n                        javaBeans.add(groupId, result);\n                    }\n\n                    Class<?> clazz = result.getSourceType();\n                    if (clazz != null && clazz.isEnum()) {\n                        enumClasses.add(clazz);\n                    }\n                    List<ApiResultObject> children = result.getChildren();\n                    if (children != null) {\n                        for (ApiResultObject child : children) {\n                            clazz = child.getSourceType();\n                            if (clazz != null && clazz.isEnum()) {\n                                enumClasses.add(clazz);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        model.put(\"methods\", methods);\n\n        return model;\n    }\n\n    private MethodInfo toMethodInfo(ApiDocObject doc, CodeConfig config, Set<String> imports) {\n        MethodInfo method = new MethodInfo();\n        method.setName(doc.getId());\n\n        String comment = doc.getComment().javadoc(1);\n        method.setComment(comment);\n\n        List<String> annos = new ArrayList<>();\n        RequestMethod type = doc.getMethods()[0];\n        if (type == RequestMethod.POST) {\n            annos.add(\"@FormUrlEncoded\");\n        }\n        String path = doc.getPaths()[0];\n        if (!path.endsWith(\"/\")) {\n            path = path + \"/\";\n        }\n        String anno = \"@\" + type.name() + \"(\\\"\" + path + \"\\\")\";\n        annos.add(anno);\n        method.setAnnos(annos);\n\n        List<ApiParamObject> srcParams = new ArrayList<>();\n        List<ApiParamObject> extraPrams = config.getExtraPrams(doc);\n        if (extraPrams != null) {\n            srcParams.addAll(extraPrams);\n        }\n        if (doc.getParams() != null) {\n            srcParams.addAll(doc.getParams());\n        }\n\n        List<ParamInfo> params = new ArrayList<>();\n        for (int i = 0; i < srcParams.size(); i++) {\n            ApiParamObject srcParam = srcParams.get(i);\n            ParamInfo param = toParam(srcParam, doc, imports);\n            if (i < srcParams.size() - 1) {\n                param.setExpression(param.getExpression() + \", \");\n            }\n            params.add(param);\n        }\n        method.setParams(params);\n\n        // 确定返回类型的描述。\n        String returnClass = null;\n        List<ApiResultObject> results = doc.getResults();\n        if (results != null && results.size() > 0) {\n            ApiResultObject result = results.get(0);\n            ApiDataType dataType = result.getDataType();\n            if (dataType != null) {\n                if (dataType == ApiDataType.ARRAY) {\n                    returnClass = \"List<\" + result.getSourceType().getSimpleName() + \">\";\n                } else {\n                    returnClass = result.getSourceType().getSimpleName();\n                }\n            }\n        }\n        if (returnClass == null) {\n            Class<?> returnType = doc.getSourceMethod().getReturnType();\n            if (returnType != null && returnType != void.class) {\n                returnClass = returnType.getSimpleName();\n            }\n        }\n        method.setReturnClass(returnClass);\n\n        return method;\n    }\n\n    private ParamInfo toParam(ApiParamObject srcParam, ApiDocObject doc, Set<String> imports) {\n        ParamInfo param = new ParamInfo();\n\n        String id = srcParam.getId();\n        param.setId(id);\n\n        String comment = srcParam.getComment().javadoc(1);\n        param.setComment(comment);\n\n        StringBuffer expression = new StringBuffer();\n\n        RequestMethod requestMethod = doc.getMethods()[0];\n        ApiParamLocation location = srcParam.getLocation();\n        String annoName = toParamAnnoName(location, requestMethod);\n        expression.append(\"@\").append(annoName) //\n                .append(\"(\\\"\").append(id).append(\"\\\")\");\n\n        Class<?> paramClass = Classes.toWrapType(srcParam.getSourceType());\n        CodeUtils.addImport(paramClass, imports);\n        expression.append(\" \").append(paramClass.getSimpleName());\n        expression.append(\" \").append(id);\n\n        param.setExpression(expression.toString());\n\n        return param;\n    }\n\n    private String toParamAnnoName(ApiParamLocation location, RequestMethod requestMethod) {\n        if (location == ApiParamLocation.RequestHeader) {\n            return \"Header\";\n        } else if (location == ApiParamLocation.PathVariable) {\n            return \"Path\";\n        } else if (location == ApiParamLocation.RequestParam) {\n            if (requestMethod == RequestMethod.POST) {\n                return \"Field\";\n            } else {\n                return \"Query\";\n            }\n        } else if (location == ApiParamLocation.CookieValue) {\n            return \"Header\";\n        } else if (location == ApiParamLocation.RequestPart) {\n            return \"Part\";\n        } else {\n            throw new RuntimeException(\"ApiParamLocation\" +\n                    \" location unsupported: \" + location);\n        }\n    }\n\n    public static final class ParamInfo {\n\n        private String id;\n\n        private String comment;\n\n        private String expression;\n\n        public String getId() {\n            return id;\n        }\n\n        public void setId(String id) {\n            this.id = id;\n        }\n\n        public String getComment() {\n            return comment;\n        }\n\n        public void setComment(String comment) {\n            this.comment = comment;\n        }\n\n        public String getExpression() {\n            return expression;\n        }\n\n        public void setExpression(String expression) {\n            this.expression = expression;\n        }\n\n    }\n\n    public static final class MethodInfo {\n\n        private List<ParamInfo> params;\n\n        private List<String> annos;\n\n        private String comment;\n\n        private String name;\n\n        private String returnClass;\n\n        public String getReturnClass() {\n            return returnClass;\n        }\n\n        public void setReturnClass(String returnClass) {\n            this.returnClass = returnClass;\n        }\n\n        public List<ParamInfo> getParams() {\n            return params;\n        }\n\n        public void setParams(List<ParamInfo> params) {\n            this.params = params;\n        }\n\n        public List<String> getAnnos() {\n            return annos;\n        }\n\n        public void setAnnos(List<String> annos) {\n            this.annos = annos;\n        }\n\n        public String getComment() {\n            return comment;\n        }\n\n        public void setComment(String comment) {\n            this.comment = comment;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n    }\n}\n\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/config/Api2DocConfiguration.java",
    "content": "package com.terran4j.commons.api2doc.config;\n\nimport com.terran4j.commons.api2doc.codewriter.RetrofitCodeWriter;\nimport com.terran4j.commons.api2doc.controller.Api2DocController;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport com.terran4j.commons.api2doc.meta.ApiMetaService;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * 可以通过配置 terran4j.api2doc.enabled 来\n * 启用或禁用文档服务。\n */\n@ConditionalOnExpression(\"${api2doc.enabled:true}\")\n@ComponentScan(basePackageClasses = {\n        Api2DocController.class,\n        Api2DocCollector.class,\n        RetrofitCodeWriter.class,\n        ApiMetaService.class\n})\n@Configuration\npublic class Api2DocConfiguration {\n\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/config/EnableApi2Doc.java",
    "content": "package com.terran4j.commons.api2doc.config;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.springframework.context.annotation.Import;\n\n\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@Import(Api2DocConfiguration.class)\npublic @interface EnableApi2Doc {\n\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/Api2DocController.java",
    "content": "package com.terran4j.commons.api2doc.controller;\n\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocProperties;\nimport com.terran4j.commons.api2doc.impl.Api2DocService;\nimport com.terran4j.commons.api2doc.impl.DocMenuBuilder;\nimport com.terran4j.commons.api2doc.impl.DocPageBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RequestParam;\n\nimport java.util.List;\nimport java.util.Map;\n\n@Controller\n@RequestMapping(value = \"/api2doc\")\npublic class Api2DocController {\n\n    private static final Logger log = LoggerFactory.getLogger(Api2DocController.class);\n\n    @Autowired\n    private DocMenuBuilder docMenuBuilder;\n\n    @Autowired\n    private DocPageBuilder docPageBuilder;\n\n    @Autowired\n    private Api2DocService apiDocService;\n\n    @Autowired\n    private Api2DocProperties api2DocProperties;\n\n    /**\n     * http://localhost:8080/api2doc/home.html\n     * 整个文档页面，包含顶部标题栏、左侧菜单栏、右侧用 iframe 嵌入的内容区。\n     */\n    @RequestMapping(value = \"/home.html\", method = RequestMethod.GET)\n    public String home(@RequestParam(value = \"p\", required = false) String p,\n                       Map<String, Object> model) throws Exception {\n\n        String title = api2DocProperties.getApi2docTitle();\n        if (StringUtils.isEmpty(title)) {\n            String serviceName = api2DocProperties.getServiceName();\n            if (StringUtils.hasText(serviceName)) {\n                title = serviceName.trim() + \"——接口文档\";\n            }\n        }\n        if (StringUtils.isEmpty(title)) {\n            title = \"Api2Doc 接口文档\";\n        }\n        model.put(\"title\", title);\n\n        String icon = api2DocProperties.getApi2docIcon();\n        if (StringUtils.hasText(icon)) {\n            model.put(\"icon\", icon);\n        }\n\n        List<MenuData> menus = docMenuBuilder.getMenuGroups();\n        model.put(\"menus\", menus);\n\n        // 当前要显示的内容。\n        String docPath = getDocPath(p);\n        model.put(\"docPath\", docPath);\n        model.put(\"v\", apiDocService.getComponentVersion());\n\n        p = p == null ? \"\" : p;\n        model.put(\"p\", p);\n\n        if (log.isInfoEnabled()) {\n            log.info(\"request home.html, model:\\n{}\", model);\n        }\n        return \"api2doc/home\";\n    }\n\n    private String getDocPath(String p) {\n        String docPath = null;\n        if (StringUtils.hasText(p)) {\n            String[] strs = p.split(\"-\");\n            if (strs.length >= 3) {\n                String docType = strs[0];\n                String docGroup = strs[1];\n                String docId = strs[2];\n                docPath = String.format(\"/api2doc/%s/%s/%s.html\",\n                        docType, docGroup, docId);\n            }\n        }\n\n        if (docPath == null) {\n            docPath = \"/api2doc/welcome.html\";\n        }\n\n        return apiDocService.addAppDocVersion(docPath);\n    }\n\n    /**\n     * http://localhost:8080/api2doc/welcome.html\n     * 文档首页内容。\n     */\n    @RequestMapping(value = \"/welcome.html\", method = RequestMethod.GET)\n    public String welcome(Map<String, Object> model) throws Exception {\n        String md = docPageBuilder.loadMdFromResource(\"welcome.md\");\n        return md2HtmlPage(md, null, model);\n    }\n\n    /**\n     * http://localhost:8080/api2doc/overview.html\n     */\n    @RequestMapping(value = \"/md/{folderId}/{docId}.html\", method = RequestMethod.GET)\n    public String md(@PathVariable(\"folderId\") String folderId,\n                     @PathVariable(\"docId\") String docId,\n                     Map<String, Object> model) throws Exception {\n        String md = docPageBuilder.loadMdFromResource(folderId, docId);\n        return md2HtmlPage(md, null, model);\n    }\n\n    @RequestMapping(value = \"/api/{fid}/{id}.html\", method = RequestMethod.GET)\n    public String api2doc(\n            @PathVariable(\"fid\") String folderId, @PathVariable(\"id\") String id,\n            Map<String, Object> model) throws Exception {\n        ApiDocObject doc = apiDocService.getDocObject(folderId, id);\n        String md = docPageBuilder.doc2Md(doc);\n        String title = doc.getName();\n        return md2HtmlPage(md, title, model);\n    }\n\n    @RequestMapping(value = \"/test/{fid}/{id}.html\", method = RequestMethod.GET)\n    public String api2test(\n            @PathVariable(\"fid\") String folderId, @PathVariable(\"id\") String id,\n            Map<String, Object> model) throws Exception {\n        ApiDocObject doc = apiDocService.getDocObject(folderId, id);\n        model.put(\"doc\", doc);\n        model.put(\"v\", apiDocService.getComponentVersion());\n//        String md = docPageBuilder.doc2Md(doc);\n//        String title = doc.getName();\n        return \"api2doc/test\";\n    }\n\n    public String md2HtmlPage(String md, String title,\n                              Map<String, Object> model) throws Exception {\n        if (title != null) {\n            model.put(\"title\", title);\n        }\n        String html = docPageBuilder.md2Html(md);\n        model.put(\"content\", html);\n\n        model.put(\"v\", apiDocService.getComponentVersion());\n\n        return \"api2doc/doc\";\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/ApiEntry.java",
    "content": "package com.terran4j.commons.api2doc.controller;\n\npublic class ApiEntry {\n\n    private String key;\n\n    private String value;\n\n    public String getKey() {\n        return key;\n    }\n\n    public void setKey(String key) {\n        this.key = key;\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n    public void setValue(String value) {\n        this.value = value;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        ApiEntry apiEntry = (ApiEntry) o;\n\n        if (key != null ? !key.equals(apiEntry.key) : apiEntry.key != null) return false;\n        return value != null ? value.equals(apiEntry.value) : apiEntry.value == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = key != null ? key.hashCode() : 0;\n        result = 31 * result + (value != null ? value.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/ApiInfo.java",
    "content": "package com.terran4j.commons.api2doc.controller;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class ApiInfo {\n\n    String[] methods;\n\n    String defaultMethod;\n\n    String url;\n\n    List<ApiEntry> params;\n\n    List<ApiEntry> headers;\n\n    public String[] getMethods() {\n        return methods;\n    }\n\n    public void setMethods(String[] methods) {\n        this.methods = methods;\n    }\n\n    public String getDefaultMethod() {\n        return defaultMethod;\n    }\n\n    public void setDefaultMethod(String defaultMethod) {\n        this.defaultMethod = defaultMethod;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public List<ApiEntry> getParams() {\n        return params;\n    }\n\n    public void setParams(List<ApiEntry> params) {\n        this.params = params;\n    }\n\n    public List<ApiEntry> getHeaders() {\n        return headers;\n    }\n\n    public void setHeaders(List<ApiEntry> headers) {\n        this.headers = headers;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        ApiInfo apiInfo = (ApiInfo) o;\n\n        // Probably incorrect - comparing Object[] arrays with Arrays.equals\n        if (!Arrays.equals(methods, apiInfo.methods)) return false;\n        if (defaultMethod != null ? !defaultMethod.equals(apiInfo.defaultMethod) : apiInfo.defaultMethod != null)\n            return false;\n        if (url != null ? !url.equals(apiInfo.url) : apiInfo.url != null) return false;\n        if (params != null ? !params.equals(apiInfo.params) : apiInfo.params != null) return false;\n        return headers != null ? headers.equals(apiInfo.headers) : apiInfo.headers == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = Arrays.hashCode(methods);\n        result = 31 * result + (defaultMethod != null ? defaultMethod.hashCode() : 0);\n        result = 31 * result + (url != null ? url.hashCode() : 0);\n        result = 31 * result + (params != null ? params.hashCode() : 0);\n        result = 31 * result + (headers != null ? headers.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/ApiMetaController.java",
    "content": "package com.terran4j.commons.api2doc.controller;\n\nimport com.terran4j.commons.api2doc.meta.ApiMetaService;\nimport com.terran4j.commons.api2doc.meta.ClassMeta;\nimport com.terran4j.commons.restpack.RestPackController;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport java.util.List;\n\n@RestPackController\n@RequestMapping(value = \"/api2doc/meta\")\npublic class ApiMetaController {\n\n    @Autowired\n    private ApiMetaService apiMetaService;\n\n    @RequestMapping(value = \"/classes\", method = RequestMethod.GET)\n    public List<ClassMeta> getClassMetaList() throws Exception {\n        return apiMetaService.toClassMetaList();\n    }\n\n    @RequestMapping(value = \"/apiInfo/{fid}/{id}\", method = RequestMethod.GET)\n    public ApiInfo getApiInfo(@PathVariable String fid, @PathVariable String id) throws Exception {\n        return apiMetaService.toApiInfo(fid, id);\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/controller/MenuData.java",
    "content": "package com.terran4j.commons.api2doc.controller;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.util.Strings;\n\nimport java.util.List;\n\npublic class MenuData implements Comparable<MenuData>{\n\n    private boolean folder;\n\n    private int order = Api2Doc.DEFAULT_ORDER;\n\n    private String id;\n\n    private String index;\n\n    private String name;\n\n    private String url;\n\n    private List<MenuData> children;\n\n    public int getOrder() {\n        return order;\n    }\n\n    public void setOrder(int order) {\n        this.order = order;\n    }\n\n    public final boolean isFolder() {\n        return folder;\n    }\n\n    public final void setFolder(boolean folder) {\n        this.folder = folder;\n    }\n\n    public final String getId() {\n        return id;\n    }\n\n    public final void setId(String id) {\n        this.id = id;\n    }\n\n    public final String getIndex() {\n        return index;\n    }\n\n    public final void setIndex(String index) {\n        this.index = index;\n    }\n\n    public final String getName() {\n        return name;\n    }\n\n    public final void setName(String name) {\n        this.name = name;\n    }\n\n    public final List<MenuData> getChildren() {\n        return children;\n    }\n\n    public final void setChildren(List<MenuData> children) {\n        this.children = children;\n    }\n\n    public final String getUrl() {\n        return url;\n    }\n\n    public final void setUrl(String url) {\n        this.url = url;\n    }\n\n    public final String toString() {\n        return Strings.toString(this);\n    }\n\n    @Override\n    public int compareTo( MenuData other) {\n        MenuData o1 = this;\n        MenuData o2 = other;\n\n        if (o1.getOrder() < o2.getOrder()) {\n            return -1;\n        }\n        if (o1.getOrder() > o2.getOrder()) {\n            return 1;\n        }\n        return 0;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiDataType.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\nimport com.fasterxml.jackson.databind.JsonMappingException;\nimport com.fasterxml.jackson.module.jsonSchema.JsonSchema;\nimport com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;\nimport com.terran4j.commons.util.Jsons;\n\n/**\n * api的基本数据类型。\n *\n * @author jiangwei\n */\npublic enum ApiDataType {\n\n    BOOLEAN(\"boolean\") {\n        @Override\n        public String getDefault() {\n            return \"false\";\n        }\n\n        @Override\n        public Object parseValue(String text) {\n            return Boolean.parseBoolean(text);\n        }\n\n        @Override\n        public boolean isSimpleType() {\n            return true;\n        }\n\n        @Override\n        public boolean isArrayType() {\n            return false;\n        }\n\n        @Override\n        public boolean isObjectType() {\n            return false;\n        }\n    },\n\n    INT(\"int\") {\n        @Override\n        public String getDefault() {\n            return \"0\";\n        }\n\n        @Override\n        public Object parseValue(String text) {\n            return Integer.parseInt(text);\n        }\n\n        @Override\n        public boolean isSimpleType() {\n            return true;\n        }\n\n        @Override\n        public boolean isArrayType() {\n            return false;\n        }\n\n        @Override\n        public boolean isObjectType() {\n            return false;\n        }\n    },\n\n    LONG(\"long\") {\n        @Override\n        public String getDefault() {\n            return \"0\";\n        }\n\n        @Override\n        public Object parseValue(String text) {\n            return Long.parseLong(text);\n        }\n\n        @Override\n        public boolean isSimpleType() {\n            return true;\n        }\n\n        @Override\n        public boolean isArrayType() {\n            return false;\n        }\n\n        @Override\n        public boolean isObjectType() {\n            return false;\n        }\n    },\n\n    NUMBER(\"number\") {\n        @Override\n        public String getDefault() {\n            return \"0.1\";\n        }\n\n        @Override\n        public Object parseValue(String text) {\n            return Double.parseDouble(text);\n        }\n\n        @Override\n        public boolean isSimpleType() {\n            return true;\n        }\n\n        @Override\n        public boolean isArrayType() {\n            return false;\n        }\n\n        @Override\n        public boolean isObjectType() {\n            return false;\n        }\n    },\n\n    STRING(\"string\") {\n        @Override\n        public String getDefault() {\n            return \"my-string\";\n        }\n\n        @Override\n        public Object parseValue(String text) {\n            return text;\n        }\n\n        @Override\n        public boolean isSimpleType() {\n            return true;\n        }\n\n        @Override\n        public boolean isArrayType() {\n            return false;\n        }\n\n        @Override\n        public boolean isObjectType() {\n            return false;\n        }\n    },\n\n    ARRAY(\"array\") {\n        @Override\n        public String getDefault() {\n            return \"[]\";\n        }\n\n        @Override\n        public Object parseValue(String text) {\n            throw new UnsupportedOperationException(\"array can't parse from text: \" + text);\n        }\n\n        @Override\n        public boolean isSimpleType() {\n            return false;\n        }\n\n        @Override\n        public boolean isArrayType() {\n            return true;\n        }\n\n        @Override\n        public boolean isObjectType() {\n            return false;\n        }\n    },\n\n    OBJECT(\"object\") {\n        @Override\n        public String getDefault() {\n            return \"{}\";\n        }\n\n        @Override\n        public Object parseValue(String text) {\n            throw new UnsupportedOperationException(\"object can't parse from text: \" + text);\n        }\n\n        @Override\n        public boolean isSimpleType() {\n            return false;\n        }\n\n        @Override\n        public boolean isArrayType() {\n            return false;\n        }\n\n        @Override\n        public boolean isObjectType() {\n            return true;\n        }\n    };\n\n    private final String name;\n\n    ApiDataType(String name) {\n        this.name = name;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public abstract String getDefault();\n\n    public abstract Object parseValue(String text);\n\n    public abstract boolean isSimpleType();\n\n    public abstract boolean isArrayType();\n\n    public abstract boolean isObjectType();\n\n    private static JsonSchemaGenerator schemaGen = null;\n\n    public static final JsonSchemaGenerator getJsonSchemaGenerator() {\n        if (schemaGen != null) {\n            return schemaGen;\n        }\n        synchronized (ApiDataType.class) {\n            if (schemaGen != null) {\n                return schemaGen;\n            }\n            try {\n                schemaGen = new JsonSchemaGenerator(Jsons.getObjectMapper());\n                return schemaGen;\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    public static ApiDataType toDataType(Class<?> clazz) {\n\n        // 日期类型，按 long 返回。\n        ApiDataType dataType = DateConverter.dateAsLongType(clazz);\n        if (dataType != null) {\n            return dataType;\n        }\n\n        try {\n            JsonSchema schema = getJsonSchemaGenerator().generateSchema(clazz);\n            return toDataType(schema);\n        } catch (JsonMappingException e) {\n            String msg = \"generate schema by class failed, class = \" + clazz.getName();\n            throw new RuntimeException(msg, e);\n        }\n    }\n\n    public static ApiDataType toDataType(JsonSchema schema) {\n        if (schema == null) {\n            return null;\n        }\n        if (schema.isBooleanSchema()) {\n            return ApiDataType.BOOLEAN;\n        }\n        if (schema.isIntegerSchema()) {\n            return ApiDataType.INT;\n        }\n        if (schema.isStringSchema()) {\n            return ApiDataType.STRING;\n        }\n        if (schema.isNumberSchema()) {\n            return ApiDataType.NUMBER;\n        }\n        if (schema.isObjectSchema()) {\n            return ApiDataType.OBJECT;\n        }\n        if (schema.isArraySchema()) {\n            return ApiDataType.ARRAY;\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiDocObject.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\nimport com.terran4j.commons.api2doc.impl.Api2DocObjectFactory;\nimport com.terran4j.commons.util.value.KeyedList;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport java.lang.reflect.Method;\nimport java.util.List;\n\npublic class ApiDocObject extends ApiObject {\n\n    private ApiFolderObject folder;\n\n    private String[] paths;\n\n    private Method sourceMethod;\n\n    private RequestMethod[] methods;\n\n    private String returnTypeDesc;\n\n    private List<ApiResultObject> results;\n\n    private ApiResultObject resultType;\n\n    private final KeyedList<String, ApiParamObject> params = new KeyedList<>();\n\n    private final KeyedList<String, ApiErrorObject> errors = new KeyedList<>();\n\n    public ApiResultObject getResultType() {\n        return resultType;\n    }\n\n    public void setResultType(ApiResultObject resultType) {\n        this.resultType = resultType;\n    }\n\n    public Method getSourceMethod() {\n        return sourceMethod;\n    }\n\n    public void setSourceMethod(Method sourceMethod) {\n        this.sourceMethod = sourceMethod;\n    }\n\n    public String[] getPaths() {\n        return paths;\n    }\n\n    public void setPaths(String[] paths) {\n        this.paths = paths;\n    }\n\n    public RequestMethod[] getMethods() {\n        return methods;\n    }\n\n    public void setMethods(RequestMethod[] methods) {\n        this.methods = methods;\n    }\n\n    public final List<ApiParamObject> getParams() {\n        return params.getAll();\n    }\n\n    public final ApiParamObject getParam(String id) {\n        return params.get(id);\n    }\n\n    public final void addParam(ApiParamObject param) {\n        this.params.add(param.getId(), param);\n    }\n\n    public List<ApiResultObject> getResults() {\n        return results;\n    }\n\n    public void setResults(List<ApiResultObject> results) {\n        this.results = results;\n    }\n\n    public List<ApiErrorObject> getErrors() {\n        return errors.getAll();\n    }\n\n    public void addError(ApiErrorObject error) {\n        this.errors.add(error.getId(), error);\n    }\n\n    public ApiFolderObject getFolder() {\n        return folder;\n    }\n\n    public void setFolder(ApiFolderObject folder) {\n        this.folder = folder;\n    }\n\n    public String getReturnTypeDesc() {\n        return returnTypeDesc;\n    }\n\n    public void setReturnTypeDesc(String returnTypeDesc) {\n        this.returnTypeDesc = returnTypeDesc;\n    }\n\n    public final Object toMockResult() {\n        List<ApiResultObject> results = getResults();\n        if (results != null && results.size() > 0) {\n            ApiResultObject result = results.get(0);\n            return Api2DocObjectFactory.createObject(result.getDataType(),\n                    result.getSourceType(), result.getSample().getValue());\n        }\n        if (resultType != null) {\n            return Api2DocObjectFactory.createObject(resultType.getDataType(),\n                    resultType.getSourceType(), resultType.getSample().getValue());\n        }\n        return null;\n    }\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiDocUtils.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\n\npublic class ApiDocUtils {\n\n\tpublic static final String getId(Class<?> clazz) {\n\t\tif (clazz == null) {\n\t\t\tthrow new NullPointerException();\n\t\t}\n\n\t\tApi2Doc api2doc = clazz.getAnnotation(Api2Doc.class);\n\n\t\tif (api2doc != null) {\n\t\t\tString id = api2doc.id();\n\t\t\tif (StringUtils.hasText(id)) {\n\t\t\t\treturn id;\n\t\t\t}\n\n\t\t\tString value = api2doc.value();\n\t\t\tif (StringUtils.hasText(value)) {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\n\t\treturn clazz.getName();\n\t}\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiErrorObject.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\npublic class ApiErrorObject extends ApiObject {\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiFolderObject.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\nimport com.terran4j.commons.util.value.KeyedList;\nimport org.springframework.util.StringUtils;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class ApiFolderObject extends ApiObject {\n\n    private boolean restPack = false;\n\n    private Map<String, String> mds;\n\n    private Class<?> sourceClass;\n\n    private final KeyedList<String, ApiDocObject> docs = new KeyedList<>();\n\n    public boolean isRestPack() {\n        return restPack;\n    }\n\n    public void setRestPack(boolean restPack) {\n        this.restPack = restPack;\n    }\n\n    public Map<String, String> getMds() {\n        return mds;\n    }\n\n    public void setMds(Map<String, String> mds) {\n        this.mds = mds;\n    }\n\n    public Class<?> getSourceClass() {\n        return sourceClass;\n    }\n\n    public void setSourceClass(Class<?> sourceClass) {\n        this.sourceClass = sourceClass;\n    }\n\n    public final List<ApiDocObject> getDocs() {\n        return docs.getAll();\n    }\n\n    public final ApiDocObject getDoc(String id) {\n        return docs.get(id);\n    }\n\n    public final void addDocs(List<ApiDocObject> docList) {\n        if (docList == null) {\n            return;\n        }\n        for (ApiDocObject doc : docList) {\n            addDoc(doc);\n        }\n    }\n\n    public final void addDoc(ApiDocObject doc) {\n        if (doc == null) {\n            throw new NullPointerException();\n        }\n        String id = doc.getId();\n        if (StringUtils.isEmpty(id)) {\n            throw new NullPointerException(\"doc id is empty.\");\n        }\n\n        this.docs.add(id, doc);\n    }\n\n    public static final String name2Id(String name) {\n        int hash = name.hashCode();\n        String id;\n        if (hash < 0) {\n            id = \"n\" + Math.abs(hash);\n        } else {\n            id = String.valueOf(hash);\n        }\n        return id;\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiObject.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.impl.FlexibleString;\nimport com.terran4j.commons.util.Strings;\n\npublic class ApiObject implements Comparable<ApiObject>{\n\n    private String id;\n\n    private String name;\n\n    private final FlexibleString comment = new FlexibleString();\n\n    private final FlexibleString sample = new FlexibleString();\n\n    private int order = Api2Doc.DEFAULT_ORDER;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public final int getOrder() {\n        return order;\n    }\n\n    public final void setOrder(int order) {\n        this.order = order;\n    }\n\n    public void insertComment(String comment) {\n        this.comment.insertLine(comment);\n    }\n\n    public FlexibleString getComment() {\n        return comment;\n    }\n\n    public void setComment(String comment) {\n        this.comment.setValue(comment);\n    }\n\n    public FlexibleString getSample() {\n        return sample;\n    }\n\n    public void setSample(String sample) {\n        this.sample.setValue(sample);\n    }\n\n    @Override\n    public final String toString() {\n        return Strings.toString(this);\n    }\n\n    @Override\n    public int compareTo(ApiObject other) {\n        ApiObject o1 = this;\n        ApiObject o2 = other;\n\n        // 优先按用户指定排序。\n        if (o1.getOrder() < o2.getOrder()) {\n            return -1;\n        }\n        if (o1.getOrder() > o2.getOrder()) {\n            return 1;\n        }\n\n        // 其次按 id 字符串排序。\n        return o1.getId().compareTo(o2.getId());\n    }\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiParamLocation.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.lang.reflect.AnnotatedElement;\n\n/**\n * API 参数在 HTTP 协议中的位置。\n *\n * @author jiangwei\n */\npublic enum ApiParamLocation {\n\n    RequestHeader {\n        @Override\n        boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) {\n            RequestHeader requestHeader = element.getAnnotation(RequestHeader.class);\n            if (requestHeader == null) {\n                return false;\n            }\n\n            String name = null;\n            if (StringUtils.hasText(requestHeader.value())) {\n                name = requestHeader.value();\n            }\n            if (StringUtils.hasText(requestHeader.name())) {\n                name = requestHeader.name();\n            }\n            apiParamObject.setName(name);\n\n            boolean required = requestHeader.required();\n            apiParamObject.setRequired(required);\n\n            String paramSample = requestHeader.defaultValue();\n            if (StringUtils.hasText(paramSample)) {\n                if (ValueConstants.DEFAULT_NONE.equals(paramSample)) {\n                    paramSample = \"\";\n                }\n                apiParamObject.setSample(paramSample);\n            }\n\n            return true;\n        }\n    },\n\n    RequestParam {\n        @Override\n        boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) {\n            RequestParam requestParam = element.getAnnotation(RequestParam.class);\n            if (requestParam == null) {\n                return false;\n            }\n\n            String name = null;\n            if (StringUtils.hasText(requestParam.value())) {\n                name = requestParam.value();\n            }\n            if (StringUtils.hasText(requestParam.name())) {\n                name = requestParam.name();\n            }\n            apiParamObject.setName(name);\n\n            boolean required = requestParam.required();\n            apiParamObject.setRequired(required);\n\n            String paramSample = requestParam.defaultValue();\n            if (StringUtils.hasText(paramSample)) {\n                if (ValueConstants.DEFAULT_NONE.equals(paramSample)) {\n                    paramSample = \"\";\n                }\n                apiParamObject.setSample(paramSample);\n            }\n\n            return true;\n        }\n    },\n\n    RequestPart {\n        @Override\n        boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) {\n            RequestPart requestPart = element.getAnnotation(RequestPart.class);\n            if (requestPart == null) {\n                return false;\n            }\n\n            String name = null;\n            if (StringUtils.hasText(requestPart.value())) {\n                name = requestPart.value();\n            }\n            if (StringUtils.hasText(requestPart.name())) {\n                name = requestPart.name();\n            }\n            apiParamObject.setName(name);\n\n            boolean required = requestPart.required();\n            apiParamObject.setRequired(required);\n\n            return true;\n        }\n    },\n\n    CookieValue {\n        @Override\n        boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) {\n            CookieValue cookieValue = element.getAnnotation(CookieValue.class);\n            if (cookieValue == null) {\n                return false;\n            }\n\n            String name = null;\n            if (StringUtils.hasText(cookieValue.value())) {\n                name = cookieValue.value();\n            }\n            if (StringUtils.hasText(cookieValue.name())) {\n                name = cookieValue.name();\n            }\n            apiParamObject.setName(name);\n\n            boolean required = cookieValue.required();\n            apiParamObject.setRequired(required);\n\n            String paramSample = cookieValue.defaultValue();\n            if (StringUtils.hasText(paramSample)) {\n                if (ValueConstants.DEFAULT_NONE.equals(paramSample)) {\n                    paramSample = \"\";\n                }\n                apiParamObject.setSample(paramSample);\n            }\n\n            return true;\n        }\n    },\n\n    PathVariable {\n        @Override\n        boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement element) {\n            PathVariable pathVariable = element.getAnnotation(PathVariable.class);\n            if (pathVariable == null) {\n                return false;\n            }\n\n            String name = null;\n            if (StringUtils.hasText(pathVariable.value())) {\n                name = pathVariable.value();\n            }\n            if (StringUtils.hasText(pathVariable.name())) {\n                name = pathVariable.name();\n            }\n            apiParamObject.setName(name);\n\n            boolean required = pathVariable.required();\n            apiParamObject.setRequired(required);\n\n            return true;\n        }\n    };\n\n    public static final ApiParamLocation[] API_PARAM_LOCATIONS = new ApiParamLocation[]{\n            RequestParam, PathVariable, RequestHeader, CookieValue, RequestPart\n    };\n\n    abstract boolean doCollect(ApiParamObject apiParamObject, AnnotatedElement param);\n\n    public static final void collects(ApiParamObject apiParamObject, AnnotatedElement element) {\n        ApiParamLocation currentLocation = ApiParamLocation.RequestParam;\n        for (ApiParamLocation location : API_PARAM_LOCATIONS) {\n            if (location.doCollect(apiParamObject, element)) {\n                currentLocation = location;\n                break;\n            }\n        }\n        apiParamObject.setLocation(currentLocation);\n\n        String sample = apiParamObject.getSample().getValue();\n        if (ValueConstants.DEFAULT_NONE.equals(sample)) {\n            apiParamObject.setSample(\"\");\n        }\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiParamObject.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\npublic class ApiParamObject extends ApiObject {\n\t\n\tprivate boolean required;\n\n\tprivate ApiDataType dataType;\n\t\n\tprivate ApiParamLocation location;\n\t\n\tprivate Class<?> sourceType;\n\t\n\tpublic ApiParamObject() {\n\t\tsuper();\n\t\tthis.setComment(\"\");\n\t}\n\t\n\tpublic Class<?> getSourceType() {\n\t\treturn sourceType;\n\t}\n\n\tpublic void setSourceType(Class<?> sourceType) {\n\t\tthis.sourceType = sourceType;\n\t}\n\n\tpublic String getTypeName() {\n\t\tif (dataType == null) {\n\t\t\treturn \"\";\n\t\t}\n\t\tif (dataType.isArrayType()) {\n\t\t\treturn dataType.name().toLowerCase() + \"[]\";\n\t\t}\n\t\treturn dataType.name().toLowerCase();\n\t}\n\n\tpublic ApiDataType getDataType() {\n\t\treturn dataType;\n\t}\n\n\tpublic void setDataType(ApiDataType type) {\n\t\tthis.dataType = type;\n\t}\n\n\tpublic boolean isRequired() {\n\t\treturn required;\n\t}\n\n\tpublic void setRequired(boolean required) {\n\t\tthis.required = required;\n\t}\n\t\n\tpublic final String getRequiredName() {\n\t\treturn required ? \"是\" : \"否\";\n\t}\n\n\tpublic ApiParamLocation getLocation() {\n\t\treturn location;\n\t}\n\n\tpublic void setLocation(ApiParamLocation location) {\n\t\tthis.location = location;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/ApiResultObject.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.impl.Api2DocUtils;\nimport com.terran4j.commons.api2doc.impl.ApiCommentUtils;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.value.KeyedList;\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\n\nimport java.beans.PropertyDescriptor;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * 记录所有的结果字段，它是一个复合型\n */\npublic class ApiResultObject extends ApiObject {\n\n    private static final Logger log = LoggerFactory.getLogger(ApiResultObject.class);\n\n    private ApiDataType dataType;\n\n    /**\n     * 如果类型是数组类型，此类是里面元素的类型\n     * 否则是这个类型本身。\n     */\n    private Class<?> sourceType;\n\n    private String typeName = \"\";\n\n    private String refGroupId = null;\n\n    private String groupId = null;\n\n    private String groupName = null;\n\n    private final List<ApiResultObject> children = new ArrayList<>();\n\n    public Class<?> getSourceType() {\n        return sourceType;\n    }\n\n    public void setSourceType(Class<?> sourceType) {\n        this.sourceType = sourceType;\n    }\n\n    public String getRefGroupId() {\n        return refGroupId;\n    }\n\n    public void setRefGroupId(String refGroupId) {\n        this.refGroupId = refGroupId;\n    }\n\n    public final ApiDataType getDataType() {\n        return dataType;\n    }\n\n    public final void setDataType(ApiDataType dataType) {\n        this.dataType = dataType;\n    }\n\n    public String getTypeName() {\n        return typeName;\n    }\n\n    public void setTypeName(String typeName) {\n        this.typeName = typeName;\n    }\n\n    public final List<ApiResultObject> getChildren() {\n        return children;\n    }\n\n    public final ApiResultObject getChild(String fieldName) {\n        if (children == null || StringUtils.isEmpty(fieldName)) {\n            return null;\n        }\n\n        for (ApiResultObject child : children) {\n            if (fieldName.equals(child.getId())) {\n                return child;\n            }\n        }\n\n        return null;\n    }\n\n    public final void addChild(ApiResultObject child) {\n        this.children.add(child);\n    }\n\n    public String getGroupId() {\n        return groupId;\n    }\n\n    public void setGroupId(String groupId) {\n        this.groupId = groupId;\n    }\n\n    public String getGroupName() {\n        return groupName;\n    }\n\n    public void setGroupName(String groupName) {\n        this.groupName = groupName;\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public static final String getEnumComment(Class<?> clazz) {\n        if (clazz == null) {\n            return null;\n        }\n        if (!clazz.isEnum()) {\n            return null;\n        }\n\n        StringBuffer sb = new StringBuffer(\"\\n可选值为：\");\n        Class<Enum<?>> enumClass = (Class<Enum<?>>) clazz;\n        Enum[] enums = enumClass.getEnumConstants();\n        for (Enum e : enums) {\n            String name = e.name();\n            Field field = null;\n            try {\n                field = enumClass.getDeclaredField(name);\n            } catch (NoSuchFieldException | SecurityException e1) {\n                log.error(\"Can't get field \\\"\" + name + \"\\\" from Enum: \" + clazz.getName(), e1);\n                continue;\n            }\n            ApiComment comment = field.getAnnotation(ApiComment.class);\n            String value = ApiCommentUtils.getComment(\n                    comment, null, field.getName());\n            if (value == null) {\n                value = \"\";\n            }\n\n            if (sb.length() > 0) {\n                sb.append(\"\\n\");\n            }\n            sb.append(name).append(\": \").append(value).append(\"; \");\n        }\n\n        return sb.toString();\n    }\n\n    private static final String getTypeName(Class<?> clazz, ApiDataType dataType) {\n        if (clazz.isEnum()) {\n            return ApiDataType.STRING.name().toLowerCase() + \"(枚举值)\";\n        } else if (dataType != null && dataType.isSimpleType()) {\n            return dataType.name().toLowerCase();\n        } else {\n            return clazz.getSimpleName();\n        }\n    }\n\n    /**\n     * 找到一个方法返回类型中字段，收集它的 Api2Doc 信息。\n     *\n     * @param method\n     * @param totalResults\n     * @return\n     */\n    public static final ApiResultObject parseResultType(\n            Method method, KeyedList<String, ApiResultObject> totalResults) {\n\n        if (method == null) {\n            return null;\n        }\n\n        if (totalResults == null) {\n            totalResults = new KeyedList<>();\n        }\n\n        final Class<?> clazz = method.getReturnType();\n        final ApiDataType dataType = ApiDataType.toDataType(clazz);\n        if (dataType == null) {\n            return null;\n        }\n        String typeName = getTypeName(clazz, dataType);\n\n        // 基本类型，直接处理。\n        if (dataType.isSimpleType()) {\n            return createSimple(clazz, clazz, dataType, typeName);\n        }\n\n        // 子类型。\n        Class<?> elementType = null;\n\n        // 数组类型，找到它的元素的具体类型，然后处理具体类型。\n        if (dataType.isArrayType()) {\n            elementType = Api2DocUtils.getArrayElementClass(method);\n            if (elementType == null) {\n                log.warn(\"Can't find element class by method: {}\", method);\n                return null;\n            }\n\n            ApiDataType elementDataType = ApiDataType.toDataType(elementType);\n            typeName = getTypeName(elementType, elementDataType) + \"[]\";\n\n            // 数组类型，但元素是基本类型的，也直接处理。\n            if (elementDataType != null && elementDataType.isSimpleType()) {\n                return createSimple(elementType, clazz,\n                        dataType, typeName);\n            }\n        }\n\n        // 复杂类型的情况。\n        ApiResultObject result = new ApiResultObject();\n        result.setDataType(dataType);\n        result.setSourceType(clazz);\n        result.setTypeName(typeName);\n        result.setId(\"\");\n\n        if (dataType.isObjectType()) {\n            elementType = method.getReturnType();\n        }\n\n        // 没有子类型，直接返回。\n        // TODO:  暂时不解析 Map 内部的类型。\n        if (elementType == null || Map.class.equals(elementType)) {\n            return result;\n        }\n\n        result.setSourceType(elementType);\n\n        // 没有子类型，直接返回。\n        PropertyDescriptor[] props = PropertyUtils.getPropertyDescriptors(elementType);\n        if (props == null || props.length == 0) {\n            return result;\n        }\n\n        // 根据类型生成字段集的 id 和 name 。\n        String groupId = getGroupId(elementType);\n        result.setGroupId(groupId);\n        String groupName = elementType.getSimpleName();\n        result.setGroupName(groupName);\n\n        // 加入到结果字段集中。\n        if (totalResults.containsKey(groupId)) {\n            return result;\n        } else {\n            totalResults.add(groupId, result);\n        }\n\n        // 有子类型，补充子类型信息。\n        for (PropertyDescriptor prop : props) {\n            if (Api2DocUtils.isFilter(prop, elementType)) {\n                continue;\n            }\n\n            String fieldName = prop.getName();\n            Method subMethod = prop.getReadMethod();\n\n            // 处理子类型。\n            ApiResultObject childPropResult;\n            try {\n                childPropResult = parseResultType(subMethod, totalResults);\n            } catch (Exception e) {\n                String msg = String.format(\"解析类[ %s ]的属性[ %s ]出错： %s\",\n                        elementType.getName(), fieldName, e.getMessage());\n                throw new RuntimeException(msg);\n            }\n\n            // 补充子类型信息。\n            if (childPropResult != null) {\n\n                // 补充到当前节点中。\n                result.addChild(childPropResult);\n\n                String id = prop.getName();\n                childPropResult.setId(id);\n                childPropResult.setName(id);\n\n                Class<?> childPropClass = subMethod.getReturnType();\n                ApiDataType childPropDataType = ApiDataType.toDataType(childPropClass);\n                childPropResult.setDataType(childPropDataType);\n\n                Api2Doc childApi2Doc;\n                ApiComment childApiComment;\n                String childName;\n                Field field = Classes.getField(id, elementType);\n                if (field != null) {\n                    childApiComment = field.getAnnotation(ApiComment.class);\n                    childApi2Doc = field.getAnnotation(Api2Doc.class);\n                    childName = field.getName();\n                } else {\n                    childApiComment = subMethod.getAnnotation(ApiComment.class);\n                    childApi2Doc = subMethod.getAnnotation(Api2Doc.class);\n                    childName = subMethod.getName();\n                }\n\n                ApiComment elementApiComment = elementType\n                        .getAnnotation(ApiComment.class);\n                Class<?> defaultSeeClass = ApiCommentUtils\n                        .getDefaultSeeClass(elementApiComment, null);\n\n                String comment = ApiCommentUtils.getComment(\n                        childApiComment, defaultSeeClass, childName);\n                if (comment == null) {\n                    comment = \"\";\n                }\n                childPropResult.insertComment(comment);\n\n                String sample = ApiCommentUtils.getSample(\n                        childApiComment, defaultSeeClass, childName);\n                if (sample == null) {\n                    sample = \"\";\n                }\n                childPropResult.setSample(sample);\n\n                if (childApi2Doc != null) {\n                    childPropResult.setOrder(childApi2Doc.order());\n                }\n\n                // 记录所引用的类型。\n                Class<?> childSubType = null;\n                if (childPropDataType != null) {\n                    if (childPropDataType.isArrayType()) {\n                        childSubType = Api2DocUtils.getArrayElementClass(subMethod);\n                    } else if (childPropDataType.isObjectType()) {\n                        childSubType = subMethod.getReturnType();\n                    }\n                }\n                if (childSubType != null) {\n                    String refGroupId = getGroupId(childSubType);\n                    childPropResult.setRefGroupId(refGroupId);\n                }\n            }\n        }\n\n        Collections.sort(result.getChildren());\n\n        return result;\n    }\n\n    public static final String getGroupId(Class<?> clazz) {\n        if (clazz == null) {\n            throw new NullPointerException();\n        }\n        String groupId = ApiDocUtils.getId(clazz);\n        return groupId;\n    }\n\n    private static ApiResultObject createSimple(Class<?> sourceType,\n                                                Class<?> clazz, ApiDataType dataType,\n                                                String typeName) {\n        ApiResultObject result = new ApiResultObject();\n        result.setSourceType(sourceType);\n        result.setDataType(dataType);\n        result.setTypeName(typeName);\n        result.insertComment(getEnumComment(clazz));\n        result.setId(\"\");\n        return result;\n    }\n\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/domain/DateConverter.java",
    "content": "package com.terran4j.commons.api2doc.domain;\n\nimport java.util.Date;\n\npublic class DateConverter {\n\n    public static boolean isDateType(Class<?> clazz) {\n        return clazz == Date.class || clazz == java.sql.Date.class;\n    }\n\n    public static Object dateAsLongValue(Class<?> clazz) {\n        if (isDateType(clazz)) {\n            // 这里不能用 Long 类型，因为赋值会失败。\n            return new Date();\n        }\n        return null;\n    }\n\n    public static ApiDataType dateAsLongType(Class<?> clazz) {\n        if (isDateType(clazz)) {\n            return ApiDataType.LONG;\n        }\n        return null;\n    }\n\n    public static Class<?> dateAsLongClass(Class<?> clazz) {\n        if (isDateType(clazz)) {\n            return Long.class;\n        }\n        return clazz;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocCollector.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.annotations.ApiError;\nimport com.terran4j.commons.api2doc.annotations.ApiErrors;\nimport com.terran4j.commons.api2doc.domain.*;\nimport com.terran4j.commons.restpack.RestPackController;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.value.KeyedList;\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.BeanDefinitionStoreException;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.config.BeanPostProcessor;\nimport org.springframework.core.LocalVariableTableParameterNameDiscoverer;\nimport org.springframework.core.annotation.AnnotationUtils;\nimport org.springframework.core.io.Resource;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\nimport java.beans.PropertyDescriptor;\nimport java.io.IOException;\nimport java.lang.reflect.AnnotatedElement;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Parameter;\nimport java.util.*;\n\n\n@Service\npublic class Api2DocCollector implements BeanPostProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(Api2DocCollector.class);\n\n    private final LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer\n            = new LocalVariableTableParameterNameDiscoverer();\n\n    @Autowired\n    private Api2DocService apiDocService;\n\n    @Override\n    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {\n\n        ApiFolderObject folder;\n        try {\n            folder = toApiFolder(bean, beanName);\n        } catch (BusinessException e) {\n            throw new BeanDefinitionStoreException(\n                    \"bean上的文档信息定义出错：\" + e.getMessage());\n        }\n        if (folder == null) {\n            return bean;\n        }\n\n        String id = folder.getId();\n        ApiFolderObject existApiFolder = apiDocService.getFolder(id);\n        if (existApiFolder != null) {\n            String msg = \"@Api2Doc id值重复： \" + id;\n            throw new BeanDefinitionStoreException(msg);\n        }\n\n        if (folder != null) {\n            apiDocService.addFolder(folder);\n        }\n        return bean;\n    }\n\n    @Override\n    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {\n        return bean;\n    }\n\n    /**\n     * 解析 API 组，一组 API 对应一个 Controller 类， 其中每个 method 对应一个 api 。<br>\n     * 只要有 @ApiDoc 注解，有会生成文档，没有这个注解就不会。\n     *\n     * @param bean\n     * @param beanName\n     * @return\n     */\n    public ApiFolderObject toApiFolder(Object bean, String beanName) throws BusinessException {\n\n        Class<?> clazz = Classes.getTargetClass(bean);\n        Controller controller = AnnotationUtils.findAnnotation(clazz, Controller.class);\n        if (controller == null) {\n            // 不是 Controller 类，不用收集。\n            return null;\n        }\n        if (log.isInfoEnabled()) {\n            log.info(\"prepare to get API Info by bean:  {}\", beanName);\n        }\n\n        List<MappingMethod> methods = MappingMethod.getMappingMethods(clazz);\n        // Classes.getMethods(RequestMapping.class, clazz);\n        if (methods == null || methods.size() == 0) {\n            // 整个类中都没有任何 RequestMapping 的方法，不用收集。\n//            if (log.isInfoEnabled()) {\n//                log.info(\"No any @RequestMapping /  method, no need to get, \" +\n//                        \"beanName = {}\", beanName);\n//            }\n            return null;\n        }\n\n        Api2Doc classApi2Doc = clazz.getAnnotation(Api2Doc.class);\n        if (classApi2Doc != null && classApi2Doc.ignore()) {\n            // 整个类的文档被忽略。\n            if (log.isInfoEnabled()) {\n                log.info(\"@Api2Doc ignore = true, no need to get, \" +\n                        \"beanName = {}\", beanName);\n            }\n            return null;\n        }\n\n        List<MappingMethod> ali2DocMethods = new ArrayList<>();\n        for (MappingMethod mappingMethod : methods) {\n            Method method = mappingMethod.getMethod();\n            Api2Doc api2Doc = method.getAnnotation(Api2Doc.class);\n            if (classApi2Doc == null && api2Doc == null) {\n                // 本方法的文档被忽略。\n                continue;\n            }\n            if (api2Doc != null && api2Doc.ignore()) {\n                // 本方法的文档被忽略。\n                continue;\n            }\n            ali2DocMethods.add(mappingMethod);\n        }\n        if (classApi2Doc == null && ali2DocMethods.size() == 0) {\n            // 整个类中的方法，都忽略从 API 生成文档，不用收集。\n            if (log.isInfoEnabled()) {\n                log.info(\"all method were ignored, no need to get, beanName = {}\", beanName);\n            }\n            return null;\n        }\n\n        ApiFolderObject folder = new ApiFolderObject();\n\n        folder.setSourceClass(clazz);\n\n        String id = beanName;\n        if (classApi2Doc != null && StringUtils.hasText(classApi2Doc.value())) {\n            id = classApi2Doc.value();\n        }\n        if (classApi2Doc != null && StringUtils.hasText(classApi2Doc.id())) {\n            id = classApi2Doc.id();\n        }\n        folder.setId(id);\n        checkId(id);\n\n        String pathPattern = \"api2doc/\" + id + \"/*.md\";\n        try {\n            Resource[] resources = Classes.scanResources(pathPattern);\n            if (resources != null && resources.length > 0) {\n                Map<String, String> mds = new HashMap<>();\n                for (Resource resource : resources) {\n                    String md = resource.getFilename();\n                    mds.put(ApiFolderObject.name2Id(md), md);\n                }\n                folder.setMds(mds);\n            }\n        } catch (IOException e) {\n            String msg = \"scan classpath[\" + pathPattern + \"] failed: \" + e.getMessage();\n            throw new BeanDefinitionStoreException(msg);\n        }\n\n        if (classApi2Doc != null) {\n            folder.setOrder(classApi2Doc.order());\n        }\n\n        // API 组的名称。\n        String name = beanName;\n        RequestMapping classMapping = clazz.getAnnotation(RequestMapping.class);\n        if (classMapping != null && StringUtils.hasText(classMapping.name())) {\n            name = classMapping.name();\n        }\n        if (classApi2Doc != null && StringUtils.hasText(classApi2Doc.name())) {\n            name = classApi2Doc.name();\n        }\n        folder.setName(name);\n\n        // 这组 API 是否用了 RestPack\n        RestPackController restPackController = clazz.getAnnotation(RestPackController.class);\n        folder.setRestPack(restPackController != null);\n\n        // API 组的注释。\n        ApiComment apiComment = clazz.getAnnotation(ApiComment.class);\n        folder.setComment(ApiCommentUtils.getComment(\n                apiComment, null, null));\n\n        // 在类上的 seeClass ，是默认的。\n        Class<?> defaultSeeClass = ApiCommentUtils.getDefaultSeeClass(\n                apiComment, null);\n\n        // API 组的路径前缀。\n        String[] basePaths = getPath(classMapping);\n\n        // 根据方法生成 API 文档。\n        List<ApiDocObject> docs = new ArrayList<>();\n        for (MappingMethod method : ali2DocMethods) {\n            ApiDocObject doc = getApiDoc(method, basePaths, beanName,\n                    classApi2Doc, defaultSeeClass);\n            if (doc == null) {\n                continue;\n            }\n\n            String docId = doc.getId();\n            ApiDocObject existDoc = folder.getDoc(docId);\n            if (existDoc != null) {\n                String msg = \"文档id值重复： \" + docId + \"\\n\"\n                        + \"如果方法上没有用  @Api2Doc(id = \\\"xxx\\\") 来指定文档id，则重载方法会出现此问题。\\n\"\n                        + \"请在重载的方法上用 @Api2Doc(id = \\\"xxx\\\") 来指定一个不同的文档id\";\n                throw new BeanDefinitionStoreException(msg);\n            }\n\n            doc.setFolder(folder);\n            docs.add(doc);\n            if (log.isInfoEnabled()) {\n                log.info(\"add doc: {}/{}\", folder.getId(), docId);\n            }\n        }\n        Collections.sort(docs);\n        folder.addDocs(docs);\n\n        return folder;\n    }\n\n    ApiDocObject getApiDoc(\n            MappingMethod mappingMethod, String[] basePaths,\n            String beanName, Api2Doc classApi2Doc,\n            Class<?> defaultSeeClass) throws BusinessException {\n\n        Method method = mappingMethod.getMethod();\n\n        // 只要有 @ApiDoc 注解（无论是本方法上，还是类上），有会生成文档，没有这个注解就不会。\n        Api2Doc api2Doc = method.getAnnotation(Api2Doc.class);\n        if (api2Doc == null && classApi2Doc == null) {\n            return null;\n        }\n\n        ApiDocObject doc = new ApiDocObject();\n\n        doc.setSourceMethod(method);\n\n        // 获取文档的 id，以 @Api2Doc、方法名 为顺序获取。\n        String id = method.getName();\n        if (api2Doc != null && StringUtils.hasText(api2Doc.value())) {\n            id = api2Doc.value();\n        }\n        if (api2Doc != null && StringUtils.hasText(api2Doc.id())) {\n            id = api2Doc.id();\n        }\n        doc.setId(id);\n        checkId(id);\n\n        // 获取文档的排序。\n        if (api2Doc != null) {\n            doc.setOrder(api2Doc.order());\n        }\n\n        // 获取文档名称，按 @Api2Doc 、@Mapping、方法名的顺序获取。\n        String name = method.getName();\n        String mappingName = mappingMethod.getName();\n        if (StringUtils.hasText(mappingName)) {\n            name = mappingName;\n        }\n        if (api2Doc != null && StringUtils.hasText(api2Doc.name())) {\n            name = api2Doc.name();\n        }\n        doc.setName(name);\n\n        // 获取 API 的注释信息。\n        ApiComment apiComment = method.getAnnotation(ApiComment.class);\n        defaultSeeClass = ApiCommentUtils.getDefaultSeeClass(apiComment, defaultSeeClass);\n        String docComment = ApiCommentUtils.getComment(apiComment, defaultSeeClass, name);\n        doc.setComment(docComment);\n        String docSample = ApiCommentUtils.getSample(apiComment, defaultSeeClass, name);\n        doc.setSample(docSample);\n\n        // 获取 API 的访问路径。\n        String[] paths = mappingMethod.getPath();\n        paths = combine(basePaths, paths);\n        doc.setPaths(paths);\n\n        // 获取 HTTP 方法。\n        doc.setMethods(mappingMethod.getRequestMethod());\n\n        // 收集参数信息。\n        List<ApiParamObject> apiParams = toApiParams(method, defaultSeeClass);\n        if (apiParams != null && apiParams.size() > 0) {\n            for (ApiParamObject apiParam : apiParams) {\n                doc.addParam(apiParam);\n            }\n        }\n\n        // 收集返回值信息。\n        KeyedList<String, ApiResultObject> totalResults = new KeyedList<>();\n        ApiResultObject resultObject = ApiResultObject.parseResultType(method, totalResults);\n        if (resultObject != null) {\n            resultObject.setComment(docComment);\n            resultObject.setSample(docSample);\n        }\n        doc.setResultType(resultObject);\n        doc.setResults(totalResults.getAll());\n\n\n        // 确定返回类型的描述。\n        String returnTypeDesc = null;\n        List<ApiResultObject> results = doc.getResults();\n        if (results != null && results.size() > 0) {\n            ApiResultObject result = results.get(0);\n            ApiDataType dataType = result.getDataType();\n            if (dataType != null) {\n                if (dataType == ApiDataType.ARRAY) {\n                    returnTypeDesc = result.getSourceType().getSimpleName() + \"[]\";\n                } else {\n                    returnTypeDesc = result.getSourceType().getSimpleName();\n                }\n            }\n        }\n        if (returnTypeDesc == null) {\n            Class<?> returnType = doc.getSourceMethod().getReturnType();\n            if (returnType != null && returnType != void.class) {\n                returnTypeDesc = returnType.getSimpleName();\n            }\n        }\n        doc.setReturnTypeDesc(returnTypeDesc);\n\n        // 收集错误码信息。\n        ApiErrors errorCodes = method.getAnnotation(ApiErrors.class);\n        if (errorCodes != null && errorCodes.value() != null\n                && errorCodes.value().length > 0) {\n            for (ApiError errorCode : errorCodes.value()) {\n                ApiErrorObject error = getError(errorCode);\n                if (error == null) {\n                    continue;\n                }\n                doc.addError(error);\n            }\n        } else {\n            ApiError errorCode = method.getAnnotation(ApiError.class);\n            ApiErrorObject error = getError(errorCode);\n            if (error != null) {\n                doc.addError(error);\n            }\n        }\n\n        return doc;\n    }\n\n\n    public List<ApiParamObject> toApiParams(Method method, Class<?> defaultSeeClass) {\n        List<ApiParamObject> result = new ArrayList<>();\n        Set<String> paramIds = new HashSet<>();\n\n        Parameter[] params = method.getParameters();\n        if (params != null && params.length > 0) {\n            String[] paramNames = parameterNameDiscoverer.getParameterNames(method);\n            for (int i = 0; i < params.length; i++) {\n                Parameter param = params[i];\n\n                Class<?> paramClass = param.getType();\n                ApiComment paramClassComment = paramClass.getAnnotation(ApiComment.class);\n                if (paramClassComment != null) {\n                    // 从参数的类的属性中获取注释信息。\n                    List<ApiParamObject> paramsFromClass = toApiParams(\n                            paramClass, defaultSeeClass);\n                    for (ApiParamObject paramFromClass : paramsFromClass) {\n                        if (paramIds.contains(paramFromClass.getId())) {\n                            continue;\n                        }\n                        paramIds.add(paramFromClass.getId());\n                        result.add(paramFromClass);\n                    }\n                } else {\n                    // 从参数本身中获取注释信息。\n                    String paramName;\n                    if (paramNames != null) {\n                        paramName = paramNames[i];\n                    } else {\n                        paramName = param.getName();\n                    }\n\n                    ApiParamObject apiParamObject = toApiParam(param,\n                            paramName, param.getType(), defaultSeeClass);\n                    if (apiParamObject == null) {\n                        continue;\n                    }\n\n                    String paramId = apiParamObject.getId();\n                    if (paramIds.contains(paramId)) {\n                        String msg = \"参数id值重复： \" + paramId + \"，所在方法： \" + method;\n                        throw new BeanDefinitionStoreException(msg);\n                    }\n\n                    paramIds.add(paramId);\n                    result.add(apiParamObject);\n                }\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 从类的属性中获取注释信息。\n     *\n     * @param beanClass\n     * @param defaultSeeClass\n     * @return\n     */\n    public List<ApiParamObject> toApiParams(Class<?> beanClass, Class<?> defaultSeeClass) {\n        List<ApiParamObject> result = new ArrayList<>();\n\n        PropertyDescriptor[] props = PropertyUtils.getPropertyDescriptors(beanClass);\n        if (props == null || props.length == 0) {\n            return result;\n        }\n\n        for (PropertyDescriptor prop : props) {\n            if (Api2DocUtils.isFilter(prop, beanClass)) {\n                continue;\n            }\n\n            String fieldName = prop.getName();\n\n            Method readMethod = prop.getReadMethod();\n            if (readMethod == null) {\n                continue;\n            }\n            Class<?> fieldType = readMethod.getReturnType();\n\n            Field field = Classes.getField(fieldName, beanClass);\n            if (field == null) {\n                continue;\n            }\n\n            ApiParamObject param = toApiParam(field, fieldName, fieldType,\n                    defaultSeeClass);\n            if (param == null) {\n                continue;\n            }\n\n            result.add(param);\n        }\n\n        Collections.sort(result);\n\n        return result;\n    }\n\n    ApiErrorObject getError(ApiError errorCode) {\n        if (errorCode == null) {\n            return null;\n        }\n\n        ApiErrorObject error = new ApiErrorObject();\n        String code = errorCode.value();\n        error.setId(code);\n        error.setName(code);\n\n        checkId(code);\n\n        String comment = errorCode.comment();\n        if (comment == null) {\n            comment = BusinessException.getMessage(code);\n        }\n        if (comment == null) {\n            comment = \"\";\n        }\n        error.setComment(comment);\n        return error;\n    }\n\n    ApiParamObject toApiParam(\n            AnnotatedElement element, String elementName,\n            Class<?> elementType, Class<?> defaultSeeClass) {\n\n        ApiParamObject apiParamObject = new ApiParamObject();\n\n        ApiParamLocation.collects(apiParamObject, element);\n\n        String id = apiParamObject.getId();\n        checkId(id);\n\n        String name = apiParamObject.getName();\n        if (StringUtils.isEmpty(name)) {\n            name = elementName;\n        }\n        apiParamObject.setName(name);\n\n        apiParamObject.setId(name);\n\n        ApiComment apiComment = element.getAnnotation(ApiComment.class);\n        ApiCommentUtils.setApiComment(apiComment, defaultSeeClass, apiParamObject);\n\n        apiParamObject.setSourceType(elementType);\n\n        ApiDataType dataType = convertType(elementType);\n        apiParamObject.setDataType(dataType);\n\n        return apiParamObject;\n    }\n\n    String[] combine(String[] classPaths, String[] methodPaths) {\n        if (classPaths == null || classPaths.length == 0) {\n            return methodPaths;\n        }\n\n        List<String> paths = new ArrayList<>();\n        for (String basePath : classPaths) {\n            for (String srcPath : methodPaths) {\n                String path = basePath + srcPath;\n                if (paths.contains(path)) {\n                    continue;\n                }\n                paths.add(path);\n            }\n        }\n        return paths.toArray(new String[paths.size()]);\n    }\n\n    ApiDataType convertType(Class<?> paramType) {\n        if (paramType == null) {\n            return null;\n        }\n        paramType = Classes.toWrapType(paramType);\n\n        if (paramType.equals(Boolean.class)) {\n            return ApiDataType.BOOLEAN;\n        }\n\n        if (paramType.equals(Integer.class)\n                || paramType.equals(Short.class)\n                || paramType.equals(Byte.class)) {\n            return ApiDataType.INT;\n        }\n\n        // Date 类型返回为 long 格式。\n        ApiDataType dataType = DateConverter.dateAsLongType(paramType);\n        if (dataType != null) {\n            return dataType;\n        }\n\n        if (paramType.equals(Long.class)) {\n            return ApiDataType.LONG;\n        }\n\n        if (paramType.equals(Float.class)\n                || paramType.equals(Double.class)) {\n            return ApiDataType.NUMBER;\n        }\n\n        if (paramType.equals(String.class)\n                || paramType.equals(Character.class)\n                || paramType.equals(StringBuffer.class)\n                || paramType.equals(StringBuilder.class)) {\n            return ApiDataType.STRING;\n        }\n\n        if (Classes.isInterface(paramType, Collection.class)) {\n            return ApiDataType.ARRAY;\n        }\n\n        return ApiDataType.OBJECT;\n    }\n\n    private String[] getPath(RequestMapping mapping) {\n        Set<String> allPaths = new HashSet<>();\n\n        if (mapping != null) {\n            String[] paths = mapping.path();\n            if (paths != null && paths.length > 0) {\n                allPaths.addAll(Arrays.asList(paths));\n            }\n\n            paths = mapping.value();\n            if (paths != null && paths.length > 0) {\n                allPaths.addAll(Arrays.asList(paths));\n            }\n        }\n\n        return allPaths.toArray(new String[allPaths.size()]);\n    }\n\n\n    private void checkId(String id) {\n        // TODO: CHECK id.\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocObjectFactory.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport com.terran4j.commons.api2doc.Api2DocMocker;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDataType;\nimport com.terran4j.commons.api2doc.domain.DateConverter;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.Enums;\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\n\nimport java.beans.PropertyDescriptor;\nimport java.lang.reflect.*;\nimport java.util.*;\n\n/**\n * 根据类上的 Api2Doc 注解信息，创建 JavaBean、数组、列表等对象。\n */\npublic class Api2DocObjectFactory {\n\n    private static final Logger log = LoggerFactory.getLogger(Api2DocObjectFactory.class);\n\n    public static Object createObject(ApiDataType dataType,\n                                      Class<?> elementType, String defaultValue) {\n        if (dataType.isArrayType()) {\n            int size = getArraySize(defaultValue, 1);\n            return createList(elementType, size);\n        } else if (dataType.isObjectType()) {\n            return createBean(elementType);\n        } else {\n            if (DateConverter.isDateType(elementType)) {\n                return DateConverter.dateAsLongValue(elementType);\n            }\n            if (StringUtils.hasText(defaultValue)) {\n                return defaultValue;\n            } else {\n                return dataType.getDefault();\n            }\n        }\n    }\n\n    public static <T> T createBean(Class<T> clazz) {\n        Stack<Class<?>> classStack = new Stack<Class<?>>();\n        return createBean(clazz, null, classStack);\n    }\n\n    public static <E> List<E> createList(Class<E> clazz, int size) {\n        Stack<Class<?>> classStack = new Stack<Class<?>>();\n        return doCreateList(clazz, size, classStack);\n    }\n\n    public static <T> T[] createArray(Class<T> clazz, int size) {\n        Stack<Class<?>> classStack = new Stack<Class<?>>();\n        return doCreateArray(clazz, size, classStack);\n    }\n\n    private static <E> List<E> doCreateList(Class<E> clazz, int size, Stack<Class<?>> classStack) {\n        List<E> list = new ArrayList<>();\n\n        if (classStack.contains(clazz)) {\n            return list;\n        }\n\n        for (int i = 0; i < size; i++) {\n            E element = createBean(clazz, null, classStack);\n            list.add(element);\n        }\n        return list;\n    }\n\n    private static <T> T[] doCreateArray(Class<T> clazz, int size, Stack<Class<?>> classStack) {\n        if (size < 0) {\n            return null;\n        }\n        T[] array = (T[]) Array.newInstance(clazz, size);\n\n        if (classStack.contains(clazz)) {\n            return array;\n        }\n\n        for (int i = 0; i < size; i++) {\n            Object element = createBean(clazz, null, classStack);\n            Array.set(array, i, element);\n        }\n        return array;\n    }\n\n\n    /**\n     * 如果是 JavaBean 类，就创建这个 JavaBean 对象；<br>\n     * 如果是 List / Array 对象，则什么都不做；<br>\n     * 如果是 简单类型 对象，就创建符合这个类型的值。\n     *\n     * @param clazz 数据对象的类型。\n     * @return 填充后的数据对象。\n     */\n    private static <T> T createBean(Class<T> clazz, String defaultValue, Stack<Class<?>> classStack) {\n        if (clazz == null) {\n            return null;\n        }\n\n        final ApiDataType dataType = ApiDataType.toDataType(clazz);\n        if (dataType == null) {\n            log.warn(\"无法识别的类型, class = {}\", clazz);\n            return null;\n        }\n\n        if (dataType.isArrayType()) {\n            if (log.isInfoEnabled()) {\n                log.info(\"数组类型，createBean 不处理, class = {}\", clazz);\n            }\n            return null;\n        }\n\n        if (dataType.isSimpleType()) {\n            if (defaultValue == null) {\n                defaultValue = dataType.getDefault();\n            }\n            Object result = dataType.parseValue(defaultValue);\n            result = adaptSimpleType(result, clazz);\n            return (T) result;\n        }\n\n        // 处理 JavaBean 对象的情况。\n        if (dataType.isObjectType()) {\n\n            T object = null;\n            try {\n                object = clazz.newInstance();\n            } catch (InstantiationException e) {\n                log.warn(\"不能根据类创建对象，class = {}, 原因：{}\", clazz, e.getMessage());\n                return null;\n            } catch (IllegalAccessException e) {\n                log.warn(\"不能根据类创建对象，class = {}, 原因：{}\", clazz, e.getMessage());\n                return null;\n            }\n            if (object == null) {\n                log.warn(\"不能根据类创建对象，class = {}\", clazz);\n                return null;\n            }\n\n            // 获取 JavaBean 的属性。\n            PropertyDescriptor[] props = PropertyUtils.getPropertyDescriptors(clazz);\n            if (props == null || props.length == 0) { // 没有属性就不用处理。\n                return object;\n            }\n\n            // 之前有过此类的信息，不用再次输出。\n            if (classStack.contains(clazz)) {\n                return object;\n            }\n\n            // 有属性，设置属性值。\n            classStack.push(clazz);\n            for (PropertyDescriptor prop : props) {\n                String fieldName = prop.getName();\n                try {\n                    fillField(fieldName, object, classStack);\n                } catch (Exception e) {\n                    log.warn(\"给字段设值出错， class = {}, fieldName = {}\", clazz, fieldName);\n                }\n            }\n            classStack.pop();\n\n            return object;\n        }\n\n        log.warn(\"无法识别的类型，class = {}\", clazz);\n        return null;\n    }\n\n    private static void fillField(String name, Object bean, Stack<Class<?>> classStack) {\n        Class<?> clazz = Classes.getTargetClass(bean);\n\n        PropertyDescriptor fieldProp = null;\n        try {\n            fieldProp = PropertyUtils.getPropertyDescriptor(bean, name);\n        } catch (IllegalAccessException e) {\n            throw new RuntimeException(e);\n        } catch (InvocationTargetException e) {\n            throw new RuntimeException(e);\n        } catch (NoSuchMethodException e) {\n            throw new RuntimeException(e);\n        }\n        if (fieldProp == null) {\n            throw new RuntimeException(\"field[\" + name + \"] NOT found in class: \" + clazz);\n        }\n        if (Api2DocUtils.isFilter(fieldProp, clazz)) {\n            return;\n        }\n\n        ApiComment classApiComment = clazz.getAnnotation(ApiComment.class);\n        Class<?> defaultSeeClass = ApiCommentUtils.getDefaultSeeClass(\n                classApiComment, null);\n\n        String fieldName = fieldProp.getName();\n\n        Method getMethod = fieldProp.getReadMethod();\n        if (getMethod == null) {\n            log.warn(\"没有 getter 方法, class = {}, fieldName = {}\", clazz, fieldName);\n            return;\n        }\n\n        Method setMethod = fieldProp.getWriteMethod();\n        if (setMethod == null) {\n            log.warn(\"没有 setter 方法, class = {}, fieldName = {}\", clazz, fieldName);\n            return;\n        }\n\n        Class<?> fieldClass = getMethod.getReturnType();\n        Field field = Classes.getField(fieldName, clazz);\n        if (field == null) {\n            log.warn(\"找不到字段定义， class = {}, fieldName = {}\", clazz, fieldName);\n            return;\n        }\n\n        ApiDataType fieldDataType = ApiDataType.toDataType(fieldClass);\n        if (fieldDataType == null) {\n            log.warn(\"未知字段类型\");\n            return;\n        }\n\n        Object fieldValue = null;\n        if (fieldDataType.isSimpleType()) {\n\n            ApiComment apiComment = field.getAnnotation(ApiComment.class);\n            String defaultValue = ApiCommentUtils.getSample(\n                    apiComment, defaultSeeClass, fieldName);\n            fieldValue = createBean(fieldClass, defaultValue, classStack);\n\n            Class<?> paramType = setMethod.getParameterTypes()[0];\n            fieldValue = adaptSimpleType(fieldValue, paramType);\n        } else if (fieldDataType.isObjectType()) {\n            fieldValue = createBean(fieldClass, null, classStack);\n        } else if (fieldDataType.isArrayType()) {\n            int size = 1;\n            ApiComment apiComment = field.getAnnotation(ApiComment.class);\n            if (apiComment != null) {\n                String sizeText = ApiCommentUtils.getSample(\n                        apiComment, defaultSeeClass, field.getName());\n                size = getArraySize(sizeText, size);\n            }\n            Class<?> elementClass = getArrayElementClass(field);\n            if (fieldClass.isArray()) {\n                fieldValue = doCreateArray(elementClass, size, classStack);\n            } else if (List.class.equals(fieldClass)) {\n                fieldValue = doCreateList(elementClass, size, classStack);\n            } else {\n                log.warn(\"不支持的集合类型，目前只支持 Array OR List， class = {}, fieldName = {}\" +\n                        \", fieldClass = {}\", clazz, fieldName, fieldClass);\n            }\n        }\n\n        try {\n            setMethod.invoke(bean, fieldValue);\n        } catch (Exception e) {\n            log.warn(\"调用 setter 方法失败, \\n\" +\n                    \"clazz = {}, \\n\" +\n                    \"setMethod = {}, \\n\" +\n                    \"fieldValue = {}, \\n\" +\n                    \"失败原因： {}\", clazz, setMethod, fieldValue, e.getMessage());\n        }\n    }\n\n    private static int getArraySize(String sizeText, int defaultValue) {\n        if (StringUtils.hasText(sizeText)) {\n            try {\n                return Integer.parseInt(sizeText);\n            } catch (Exception e) {\n                log.warn(\"List 或 Array 类型的字段上，\" +\n                        \"@ApiComment 注解的 sample 属性应该是数字\" +\n                        \"（代表它在 mock 时元素的个数）, sample = {}\", sizeText);\n            }\n        }\n        return defaultValue;\n    }\n\n    private static Object adaptSimpleType(Object sourceValue, Class<?> targetType) {\n        if (Long.class.equals(targetType) || long.class.equals(targetType)) {\n            return Long.parseLong(sourceValue.toString());\n        }\n        if (Byte.class.equals(targetType) || byte.class.equals(targetType)) {\n            return Byte.parseByte(sourceValue.toString());\n        }\n        if (Short.class.equals(targetType) || short.class.equals(targetType)) {\n            return Short.parseShort(sourceValue.toString());\n        }\n        if (Float.class.equals(targetType) || float.class.equals(targetType)) {\n            return Float.parseFloat(sourceValue.toString());\n        }\n        if (DateConverter.isDateType(targetType)) {\n            return DateConverter.dateAsLongValue(targetType);\n        }\n\n        if (targetType.isEnum()) {\n            return Enums.getEnumObject(targetType, sourceValue.toString());\n        }\n\n        return sourceValue;\n    }\n\n    private static final Class<?> getArrayElementClass(Field field) {\n\n        Class<?> returnType = field.getType();\n        if (returnType.isArray()) {\n            Class<?> elementClass = returnType.getComponentType();\n            return elementClass;\n        }\n\n        if (Classes.isInterface(returnType, Collection.class)) {\n            Type type = field.getGenericType();\n            Type elementType = Api2DocUtils.getGenericType(type);\n            if (elementType instanceof Class<?>) {\n                Class<?> elementClass = (Class<?>) elementType;\n                return elementClass;\n            }\n        }\n\n        log.warn(\"不支持的数组类型\");\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocProperties.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class Api2DocProperties {\n\n    @Value(\"${server.url:http://localhost:8080}\")\n    private String serverURL;\n\n    @Value(\"${api2doc.showCurl:true}\")\n    private boolean showCurl = true;\n\n    @Value(\"${service.name:}\")\n    private String serviceName;\n\n    @Value(\"${api2doc.title:}\")\n    private String api2docTitle;\n\n    @Value(\"${api2doc.icon:}\")\n    private String api2docIcon;\n\n    public String getServerURL() {\n        return serverURL;\n    }\n\n    public boolean isShowCurl() {\n        return showCurl;\n    }\n\n    public String getServiceName() {\n        return serviceName;\n    }\n\n    public String getApi2docTitle() {\n        return api2docTitle;\n    }\n\n    public String getApi2docIcon() {\n        return api2docIcon;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocService.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.util.value.KeyedList;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Service;\n\nimport java.util.List;\n\n@Service\npublic class Api2DocService {\n\n    private static final Logger log = LoggerFactory.getLogger(Api2DocService.class);\n\n    private static final String v = String.valueOf(System.currentTimeMillis());\n\t\n\tprivate final KeyedList<String, ApiFolderObject> folders = new KeyedList<>();\n\t\n\tpublic boolean hasFolder(String id) {\n\t\treturn folders.containsKey(id);\n\t}\n\t\n\tpublic ApiFolderObject getFolder(String id) {\n\t\treturn folders.get(id);\n\t}\n\t\n\tpublic void addFolder(ApiFolderObject folder) {\n\t\tif (folder == null) {\n\t\t\tthrow new NullPointerException(\"ApiFolderObject is null\");\n\t\t}\n\t\tfolders.add(folder.getId(), folder);\n\t}\n\t\n\tpublic List<ApiFolderObject> getFolders() {\n\t\treturn folders.getAll();\n\t}\n\n    public String addAppDocVersion(String path) {\n\t    if (path.indexOf(\"?\") > 0) {\n\t        return path + \"&v=\" + getAppDocVersion();\n        } else {\n            return path + \"?v=\" + getAppDocVersion();\n        }\n    }\n\n    public String getAppDocVersion() {\n        return v;\n    }\n\n    public String getComponentVersion() {\n\t    return v;\n    }\n\n    public ApiDocObject getDocObject(String folderId, String docId) throws Exception {\n        ApiFolderObject folder = getFolder(folderId);\n        if (folder == null) {\n            log.warn(\"ApiFolder NOT Found: {}\", folderId);\n            return null;\n        }\n\n        ApiDocObject doc = folder.getDoc(docId);\n        if (doc == null) {\n            if (log.isWarnEnabled()) {\n                log.warn(\"ApiDoc NOT Found: {}\", folderId);\n            }\n            return null;\n        }\n\n        return doc;\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/Api2DocUtils.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiObject;\nimport com.terran4j.commons.api2doc.domain.ApiParamLocation;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\nimport com.terran4j.commons.restpack.RestPackIgnore;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.Encoding;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.value.ValueSource;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport java.beans.PropertyDescriptor;\nimport java.io.UnsupportedEncodingException;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\nimport java.net.URLEncoder;\nimport java.util.*;\n\npublic class Api2DocUtils {\n\n    public static String toURL(ApiDocObject doc, String serverURL) {\n        if (doc == null) {\n            throw new NullPointerException(\"doc is null.\");\n        }\n\n        boolean hasGetMethod = false;\n        RequestMethod[] methods = doc.getMethods();\n        if (methods != null) {\n            for (RequestMethod method : methods) {\n                if (method == RequestMethod.GET) {\n                    hasGetMethod = true;\n                }\n            }\n        } else {\n            hasGetMethod = true;\n        }\n        if (!hasGetMethod) {\n            // TODO: 暂时不支持非 GET 的请求。\n            return null;\n        }\n\n        String docURL = serverURL + doc.getPaths()[0];\n        List<ApiParamObject> params = doc.getParams();\n        Map<String, String> pathParams = new HashMap<>();\n        Map<String, String> getParams = new HashMap<>();\n        if (params != null) {\n            for (ApiParamObject param : params) {\n                if (param.getLocation() == ApiParamLocation.PathVariable) {\n                    String value = param.getSample().getValue();\n                    if (StringUtils.isEmpty(value)) {\n                        value = param.getDataType().getDefault();\n                    }\n                    pathParams.put(param.getId(), value);\n                }\n                if (param.getLocation() == ApiParamLocation.RequestParam) {\n                    String value = param.getSample().getValue();\n                    if (StringUtils.isEmpty(value)) {\n                        continue;\n                    }\n                    getParams.put(param.getId(), value);\n                }\n            }\n        }\n\n        if (pathParams.size() > 0) {\n            docURL = Strings.format(docURL, new ValueSource<String, String>() {\n\n                @Override\n                public String get(String key) {\n                    String value = pathParams.get(key);\n                    if (value == null) {\n                        return null;\n                    }\n                    return encode(value);\n                }\n            }, \"{\", \"}\", null);\n        }\n\n        if (getParams.size() > 0) {\n\n            List<String> keys = new ArrayList<>();\n            keys.addAll(getParams.keySet());\n            keys.sort(new Comparator<String>() {\n                @Override\n                public int compare(String o1, String o2) {\n                    return o1.compareTo(o2);\n                }\n            });\n\n            StringBuffer sb = new StringBuffer();\n            for (String key : keys) {\n                String value = getParams.get(key);\n                if (sb.length() > 0) {\n                    sb.append(\"&\");\n                }\n                sb.append(key).append(\"=\").append(encode(value));\n            }\n            docURL = docURL + \"?\" + sb.toString();\n        }\n\n        return docURL;\n    }\n\n    private static String encode(String text) {\n        try {\n            return URLEncoder.encode(text, Encoding.UTF8.getName());\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n//    public static final void setApiComment(ApiComment apiComment, ApiObject apiObject) {\n//        if (apiComment != null && StringUtils.hasText(apiComment.value())) {\n//            String commentText = getComment(apiComment);\n//            apiObject.setComment(commentText);\n//        }\n//        if (apiComment != null && StringUtils.hasText(apiComment.sample())) {\n//            if (StringUtils.hasText(apiComment.sample())) {\n//                apiObject.setSample(apiComment.sample());\n//            }\n//        }\n//    }\n//\n//    public static final String getComment(ApiComment apiComment) {\n//        String comment = apiComment.value();\n//        if (StringUtils.isEmpty(comment)) {\n//            return null;\n//        }\n//        return comment.trim();\n//    }\n//\n//    public static final String getSample(ApiComment apiComment, Class<?> clazz) throws BusinessException {\n//        String sample = apiComment.sample();\n//        if (StringUtils.isEmpty(sample)) {\n//            return null;\n//        }\n//        return sample.trim();\n//\n////        if (!(sample.startsWith(\"@\") && sample.endsWith(\"@\"))) {\n////            return sample.replaceAll(\"\\n\", \"<br/>\");\n////        }\n////\n////        String fileName = sample.substring(1, sample.length() - 1);\n////        String json = Strings.getString(clazz, fileName);\n////        if (StringUtils.isEmpty(json)) {\n////            throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n////                    .put(\"package\", clazz.getPackage().getName())\n////                    .put(\"fileName\", fileName)\n////                    .setMessage(\"在包 ${package} 中找不到文件： ${fileName}\");\n////        }\n////        return json;\n//    }\n\n    public static final Class<?> getArrayElementClass(Method method) {\n\n        Class<?> returnType = method.getReturnType();\n        if (returnType.isArray()) {\n            Class<?> elementClass = returnType.getComponentType();\n            return elementClass;\n        }\n\n        if (Classes.isInterface(returnType, Collection.class)) {\n            Type gType = method.getGenericReturnType();\n            Type elementType = getGenericType(gType);\n            if (elementType instanceof Class<?>) {\n                Class<?> elementClass = (Class<?>) elementType;\n                return elementClass;\n            }\n        }\n\n        return null;\n    }\n\n    public static final Type getGenericType(Type gType) {\n        // 如果gType是泛型类型对像。\n        if (gType instanceof ParameterizedType) {\n            ParameterizedType pType = (ParameterizedType) gType;\n            // 获得泛型类型的泛型参数\n            Type[] gArgs = pType.getActualTypeArguments();\n            return gArgs[gArgs.length - 1];\n        } else {\n            System.out.println(\"获取泛型信息失败\");\n            return null;\n        }\n    }\n\n    public static final boolean isFilter(\n            PropertyDescriptor prop, Class<?> clazz) {\n\n        String fieldName = prop.getName();\n\n        // class 只是 Java 对象自带的  Object.getClass() 方法，忽略掉。\n        if (\"class\".equals(fieldName)) {\n            return true;\n        }\n\n        // 忽略掉需要忽略的字段。\n        Field field = Classes.getField(fieldName, clazz);\n        if (field != null) {\n            if (field.getAnnotation(RestPackIgnore.class) != null) {\n                return true;\n            }\n            if (field.getAnnotation(JsonIgnore.class) != null) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/ApiCommentUtils.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiObject;\nimport com.terran4j.commons.util.Classes;\nimport org.springframework.util.StringUtils;\n\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Stack;\n\npublic class ApiCommentUtils {\n\n\n    public static Class<?> getDefaultSeeClass(ApiComment apiComment,\n                                              Class<?> previousSeeClass) {\n        if (apiComment == null) {\n            return previousSeeClass;\n        }\n\n        Class<?> defaultSeeClass = apiComment.seeClass();\n        if (defaultSeeClass == Object.class) {\n            return previousSeeClass;\n        }\n\n        return defaultSeeClass;\n    }\n\n    public static final void setApiComment(ApiComment apiComment,\n                                           Class<?> defaultSeeClass, ApiObject apiObject) {\n        String comment = getComment(apiComment, defaultSeeClass, apiObject.getId());\n        if (comment != null) {\n            apiObject.setComment(comment);\n        }\n        String sample = getSample(apiComment, defaultSeeClass, apiObject.getId());\n        if (sample != null) {\n            apiObject.setSample(sample);\n        }\n    }\n\n//    public static final ApiComment getComment(Field field, Class<?> defaultSeeClass) {\n//        ApiComment apiComment = field.getAnnotation(ApiComment.class);\n//        if (apiComment !)\n//    }\n\n    public static final String getComment(ApiComment apiComment,\n                                          Class<?> defaultSeeClass, String defaultName) {\n\n        if (apiComment != null) {\n            String comment = apiComment.value();\n            if (StringUtils.hasText(comment)) {\n                return comment.trim();\n            }\n        }\n\n        ApiComment seeComment = getSeeApiComment(\n                apiComment, defaultSeeClass, defaultName);\n        if (seeComment == null) {\n            return null;\n        }\n\n        String comment = seeComment.value();\n        if (StringUtils.hasText(comment)) {\n            return comment.trim();\n        }\n\n        return null;\n    }\n\n    public static final String getSample(ApiComment apiComment,\n                                         Class<?> defaultSeeClass, String defaultName) {\n        if (apiComment != null) {\n            String sample = apiComment.sample();\n            if (StringUtils.hasText(sample)) {\n                return sample.trim();\n            }\n        }\n\n        ApiComment seeComment = getSeeApiComment(\n                apiComment, defaultSeeClass, defaultName);\n        if (seeComment == null) {\n            return null;\n        }\n\n        String sample = seeComment.sample();\n        if (StringUtils.hasText(sample)) {\n            return sample.trim();\n        }\n\n        return null;\n    }\n\n    /**\n     * 获取可参照的 @ApiComment 注解对象。\n     * @param apiComment\n     * @param defaultSeeClass\n     * @param defaultSeeField\n     * @return\n     */\n    private static ApiComment getSeeApiComment(\n            ApiComment apiComment, Class<?> defaultSeeClass, String defaultSeeField) {\n\n        Class<?> seeClass = null;\n        if (apiComment != null ) {\n            seeClass = apiComment.seeClass();\n        }\n        if (seeClass == null || seeClass == Object.class) {\n            seeClass = defaultSeeClass;\n        }\n        if (seeClass == null || seeClass == Object.class) {\n            return null;\n        }\n\n        String seeField = null;\n        if (apiComment != null) {\n            seeField = apiComment.seeField();\n        }\n        if (StringUtils.isEmpty(seeField)) {\n            seeField = defaultSeeField;\n        }\n        if (StringUtils.isEmpty(seeField)) {\n            return null;\n        }\n\n        // 记录引用过的 seeClass， 用于循环引用检测。\n        List<Class<?>> path = new ArrayList<>();\n        Field field = null;\n        ApiComment seeComment = null;\n        while (seeClass != null) {\n\n            // 循环引用检测。\n            if (path.contains(seeClass)) {\n                StringBuffer sb = new StringBuffer();\n                sb.append(\"@ApiComment 中的 seeClass 不允许循环引用：\");\n                for (int i = 0; i < path.size(); i++) {\n                    if (i > 0) {\n                        sb.append(\" --> \");\n                    }\n                    sb.append(seeClass.getSimpleName());\n                }\n                throw new RuntimeException(sb.toString());\n            }\n            path.add(seeClass);\n\n            // 寻找匹配字段中的 ApiComment 。\n            field = Classes.getField(seeField, seeClass);\n            if (field != null) {\n                seeComment = field.getAnnotation(ApiComment.class);\n                if (seeComment != null) {\n                    return seeComment;\n                }\n            }\n\n            ApiComment parentApiComment = seeClass.getAnnotation(ApiComment.class);\n            if (parentApiComment == null) {\n                break;\n            }\n            seeClass = parentApiComment.seeClass();\n        }\n\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/ClasspathFreeMarker.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.springframework.stereotype.Service;\nimport org.springframework.ui.freemarker.FreeMarkerTemplateUtils;\n\nimport com.terran4j.commons.util.Encoding;\n\nimport freemarker.cache.ClassTemplateLoader;\nimport freemarker.cache.TemplateLoader;\nimport freemarker.template.Configuration;\nimport freemarker.template.Template;\nimport freemarker.template.TemplateException;\n\n@Service\npublic class ClasspathFreeMarker {\n\n\tprivate final Map<String, Template> templates = new HashMap<String, Template>();\n\n\tprivate final Configuration freeMarker;\n\n\tpublic ClasspathFreeMarker() {\n\t\tsuper();\n\n\t\ttry {\n\t\t\tfreeMarker = new Configuration(Configuration.VERSION_2_3_25);\n\t\t\tfreeMarker.setDefaultEncoding(Encoding.UTF8.getName());\n\t\t\tTemplateLoader ctl = new ClassTemplateLoader(getClass(), \"/\");\n\t\t\tfreeMarker.setTemplateLoader(ctl);\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\tprivate String getPath(Class<?> clazz, String fileName) {\n\t\tString path = clazz.getPackage().getName().replace('.', '/') + \"/\" + fileName;\n\t\treturn path;\n\t}\n\n\tpublic final Template getTemplate(Class<?> clazz, String fileName) {\n\t\tString path = getPath(clazz, fileName);\n\n\t\tTemplate template = templates.get(path);\n\t\tif (template != null) {\n\t\t\treturn template;\n\t\t}\n\n\t\tsynchronized (ClasspathFreeMarker.class) {\n\t\t\ttemplate = templates.get(path);\n\t\t\tif (template != null) {\n\t\t\t\treturn template;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\ttemplate = freeMarker.getTemplate(path);\n\t\t\t\ttemplates.put(path, template);\n\t\t\t\treturn template;\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new RuntimeException(e);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic final String build(Template template, Map<String, Object> model) //\n\t\t\tthrows IOException, TemplateException {\n\t\tif (!templates.containsValue(template)) {\n\t\t\tString msg = \"Can't build from the tempate which NOT get by calling this method:\\n\"\n\t\t\t\t\t+ \"ClasspathFreeMarker.getTemplate(Class<?> clazz, String fileName)\";\n\t\t\tthrow new UnsupportedOperationException(msg);\n\t\t}\n\t\tString html = FreeMarkerTemplateUtils.processTemplateIntoString( //\n\t\t\t\ttemplate, model);\n\t\treturn html;\n\t}\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/CurlBuilder.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiParamLocation;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\nimport com.terran4j.commons.util.Encoding;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.value.KeyedList;\nimport com.terran4j.commons.util.value.ValueSource;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\npublic class CurlBuilder {\n\n    private static final Logger log = LoggerFactory.getLogger(CurlBuilder.class);\n\n    private static final String enter = \" \\\\\\n\";\n\n    public static String toCurl(ApiDocObject docObject, String serverURL) {\n\n        final List<ApiParamObject> allParams = docObject.getParams();\n        final KeyedList<String, String> headers = new KeyedList<>();\n        final KeyedList<String, String> params = new KeyedList<>();\n        final KeyedList<String, String> cookies = new KeyedList<>();\n        final Map<String, String> pathVars = new HashMap<>();\n        final KeyedList<String, String> parts = new KeyedList<>();\n\n        if (allParams.size() > 0) {\n            for (ApiParamObject param : allParams) {\n                String key = param.getId();\n\n                String value = param.getSample().getValue();\n                if (StringUtils.isEmpty(value)) {\n                    Class<?> paramType = param.getSourceType();\n                    if (paramType == String.class) {\n                        value = key;\n                    } else {\n                        value = param.getDataType().getDefault();\n                    }\n                }\n\n                if (param.getLocation() == ApiParamLocation.RequestParam) {\n                    params.add(key, value);\n                }\n                if (param.getLocation() == ApiParamLocation.PathVariable) {\n                    pathVars.put(key, value);\n                }\n                if (param.getLocation() == ApiParamLocation.RequestHeader) {\n                    headers.add(key, value);\n                }\n                if (param.getLocation() == ApiParamLocation.CookieValue) {\n                    cookies.add(key, value);\n                }\n                if (param.getLocation() == ApiParamLocation.RequestPart) {\n                    parts.add(key, value);\n                }\n            }\n        }\n\n        RequestMethod[] requestMethods = docObject.getMethods();\n        RequestMethod requestMethod = requestMethods[0];\n        StringBuilder sb = new StringBuilder(\"curl\");\n        if (parts.size() == 0) {\n            // 没有上传文件，才指定方法。\n            sb.append(\" -X \").append(requestMethod.name());\n        }\n        sb.append(enter);\n\n        // 将 Header 参数拼接起来。\n        if (headers.size() > 0) {\n            for (int i = 0; i < headers.size(); i++) {\n                String key = headers.getKey(i);\n                String value = headers.get(i);\n                sb.append(\" -H \\\"\").append(key).append(\": \").append(value)\n                        .append(\"\\\"\").append(enter);\n            }\n        }\n\n        if (cookies.size() > 0) {\n            sb.append(\" -b \\\"\");\n            sb.append(joinText(cookies, \";\", \"=\"));\n            sb.append(\"\\\"\").append(enter);\n        }\n\n        // 将 URL 中的 {xx} 变量用参数的示例值代替。\n        String url = serverURL + docObject.getPaths()[0];\n        if (pathVars.size() > 0) {\n            ValueSource<String, String> vars = new ValueSource<String, String>() {\n                @Override\n                public String get(String key) {\n                    return encode(pathVars.get(key));\n                }\n            };\n            url = Strings.format(url, vars, \"{\", \"}\", null);\n        }\n\n        // 将“参数”拼起来。\n        if (params.size() > 0) {\n            if (parts.size() > 0) {\n                // 参数按 multipart 的方法传。\n                for (int i = 0; i < params.size(); i++) {\n                    String key = params.getKey(i);\n                    String value = params.get(i);\n                    sb.append(\" -F \\\"\").append(key).append(\"=\").append(value)\n                            .append(\"\\\"\").append(enter);\n                }\n            } else if (requestMethod == RequestMethod.POST) {\n                // 参数按 post 的方法传。\n                sb.append(\" -d \\\"\").append(joinText(params, \"&\", \"=\"))\n                        .append(\"\\\"\").append(enter);\n            } else {\n                // 参数附加到 URL 后面。\n                url += (\"?\" + joinText(params, \"&\", \"=\"));\n            }\n        }\n\n        // 追加 multipart 参数。\n        if (parts.size() > 0) {\n            for (int i = 0; i < parts.size(); i++) {\n                String key = parts.getKey(i);\n                String value = parts.get(i);\n                sb.append(\" -F \\\"\").append(key).append(\"=@\").append(value)\n                        .append(\"\\\"\").append(enter);\n            }\n        }\n\n        // 将 URL 拼接起来。\n        sb.append(\" \\\"\").append(url).append(\"\\\"\");\n\n        String curl = sb.toString();\n        if (log.isInfoEnabled()) {\n            log.info(\"doc[{}]'s curl:\\n{}\", docObject.getId(), curl);\n        }\n        return curl;\n    }\n\n    /**\n     * 连接成字符串，并将 value 值进行 URL 编码。\n     */\n    public static final String joinText(KeyedList<String, String> params,\n                                        String joiner, String splitter) {\n        StringBuffer sb = new StringBuffer();\n        boolean first = true;\n        Iterator<String> it = params.keySet().iterator();\n        while (it.hasNext()) {\n            String key = it.next();\n            String value = params.get(key);\n            if (!first) {\n                sb.append(joiner);\n            }\n            sb.append(key).append(splitter).append(encode(value));\n            first = false;\n        }\n        return sb.toString();\n    }\n\n    private static String encode(String value) {\n        try {\n            // 对于空格的编码，有的地方不认 + ，所以统一转成： %20\n            String str = URLEncoder.encode(value, Encoding.UTF8.getName());\n            return str.replaceAll(\"\\\\+\", \"%20\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/DocMenuBuilder.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport com.terran4j.commons.api2doc.controller.MenuData;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Service\npublic class DocMenuBuilder {\n\n    @Autowired\n    private Api2DocService apiDocService;\n\n    public List<MenuData> getMenuGroups() {\n        List<MenuData> menuGroups = new ArrayList<>();\n\n        List<ApiFolderObject> folders = apiDocService.getFolders();\n        if (folders == null || folders.size() == 0) {\n            return menuGroups;\n        }\n\n        for (ApiFolderObject folder : folders) {\n            MenuData menuGroup = getMenuGroup(folder);\n            menuGroups.add(menuGroup);\n        }\n\n        Collections.sort(menuGroups);\n\n        return menuGroups;\n    }\n\n    public MenuData getMenuGroup(ApiFolderObject folder) {\n        String folderId = folder.getId();\n        String folderName = folder.getName();\n\n        MenuData menuGroup = new MenuData();\n        menuGroup.setId(folderId);\n        menuGroup.setIndex(folderId);\n        menuGroup.setName(folderName);\n        menuGroup.setFolder(true);\n        menuGroup.setOrder(folder.getOrder());\n\n        List<MenuData> children = new ArrayList<>();\n\n        Map<String, String> mds = folder.getMds();\n        if (mds != null && mds.size() > 0) {\n            for (String md : mds.values()) {\n                MenuData menu = getMenu(md, folderId);\n                children.add(menu);\n            }\n        }\n\n        List<ApiDocObject> docs = folder.getDocs();\n        if (docs != null) {\n            for (ApiDocObject doc : docs) {\n                MenuData menu = getMenu(doc, folderId);\n                children.add(menu);\n            }\n        }\n\n        Collections.sort(children);\n        menuGroup.setChildren(children);\n        return menuGroup;\n    }\n\n    public MenuData getMenu(String mdFileName, String folderId) {\n        int offset = mdFileName.indexOf(\"-\");\n        String orderText = mdFileName.substring(0, offset);\n        int order = Integer.parseInt(orderText);\n        String docName = mdFileName.substring(offset + 1,\n                mdFileName.length() - \".md\".length());\n        String docId = ApiFolderObject.name2Id(mdFileName);\n        MenuData menu = new MenuData();\n\n        String pageId = \"md-\" + folderId + \"-\" + docId;\n        menu.setId(pageId);\n        menu.setIndex(pageId);\n        String url = getPageURL(pageId);\n        menu.setUrl(url);\n\n        menu.setFolder(false);\n        menu.setName(docName);\n        menu.setOrder(order);\n\n        return menu;\n    }\n\n    public MenuData getMenu(ApiDocObject doc, String folderId) {\n        MenuData menu = new MenuData();\n\n        String pageId = \"api-\" + folderId + \"-\" + doc.getId();\n        menu.setId(pageId);\n        menu.setIndex(pageId);\n        String url = getPageURL(pageId);\n        menu.setUrl(url);\n\n        menu.setFolder(false);\n        menu.setName(doc.getName());\n        menu.setOrder(doc.getOrder());\n\n        return menu;\n    }\n\n    private String getPageURL(String pageId) {\n        try {\n            String p = URLEncoder.encode(pageId, \"UTF-8\");\n            String path = \"/api2doc/home.html?p=\" + p;\n            return apiDocService.addAppDocVersion(path);\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(\"Can't encoding: \" + pageId, e);\n        }\n    }\n}"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/DocPageBuilder.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.restpack.config.RestPackConfiguration;\nimport com.terran4j.commons.util.Strings;\nimport com.vladsch.flexmark.ast.Node;\nimport com.vladsch.flexmark.ext.spec.example.SpecExampleExtension;\nimport com.vladsch.flexmark.ext.tables.TablesExtension;\nimport com.vladsch.flexmark.html.HtmlRenderer;\nimport com.vladsch.flexmark.parser.Parser;\nimport com.vladsch.flexmark.parser.ParserEmulationProfile;\nimport com.vladsch.flexmark.util.KeepType;\nimport com.vladsch.flexmark.util.options.MutableDataSet;\nimport freemarker.template.Template;\nimport freemarker.template.TemplateException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\n\nimport javax.annotation.PostConstruct;\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Service\npublic class DocPageBuilder {\n\n    private static final Logger log = LoggerFactory.getLogger(DocPageBuilder.class);\n\n    private static final String FILE_DOC_MD = \"doc.md.ftl\";\n\n    @Autowired\n    private Api2DocProperties api2DocProperties;\n\n    @Autowired\n    private Api2DocService apiDocService;\n\n    @Autowired\n    private ClasspathFreeMarker freeMarker;\n\n    private Template mdTemplate = null;\n\n    private Parser parser = null;\n\n    private HtmlRenderer renderer = null;\n\n    @PostConstruct\n    public void init() {\n        try {\n            mdTemplate = freeMarker.getTemplate(DocPageBuilder.class, FILE_DOC_MD);\n\n            MutableDataSet options = new MutableDataSet();\n            options.setFrom(ParserEmulationProfile.GITHUB_DOC);\n            options.set(Parser.EXTENSIONS, Arrays.asList(\n                    TablesExtension.create(), // 表格渲染插件。\n                    SpecExampleExtension.create() // 代码渲染插件。\n            ));\n\n            // References compatibility\n            options.set(Parser.REFERENCES_KEEP, KeepType.LAST);\n\n            // Set GFM table parsing options\n            options.set(TablesExtension.COLUMN_SPANS, false) //\n                    .set(TablesExtension.MIN_HEADER_ROWS, 1) //\n                    .set(TablesExtension.MAX_HEADER_ROWS, 1) //\n                    .set(TablesExtension.APPEND_MISSING_COLUMNS, true) //\n                    .set(TablesExtension.DISCARD_EXTRA_COLUMNS, true) //\n                    .set(TablesExtension.WITH_CAPTION, false) //\n                    .set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true);\n\n            // Setup List Options for GitHub profile which is kramdown for documents\n            options.setFrom(ParserEmulationProfile.GITHUB_DOC);\n\n            // You can re-use parser and renderer instances\n            parser = Parser.builder(options).build();\n            renderer = HtmlRenderer.builder(options).build();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String md2Html(String md) throws Exception {\n        Node document = parser.parse(md);\n        String html = renderer.render(document);\n        return html;\n    }\n\n//    public String md2HtmlPage(String md, String title) throws Exception {\n//        String content = md2Html(md);\n//\n//        Map<String, Object> model = new HashMap<String, Object>();\n//        if (title != null) {\n//            model.put(\"title\", title);\n//        }\n//        model.put(\"content\", content);\n//        model.put(\"v\", apiDocService.getComponentVersion());\n//\n//        String html = freeMarker.build(docTemplate, model);\n//        return html;\n//    }\n//\n//    public ApiDocObject getDocObject(String folderId, String docId) throws Exception {\n//        ApiFolderObject folder = apiDocService.getFolder(folderId);\n//        if (folder == null) {\n//            log.warn(\"ApiFolder NOT Found: {}\", folderId);\n//            return null;\n//        }\n//\n//        ApiDocObject doc = folder.getDoc(docId);\n//        if (doc == null) {\n//            if (log.isWarnEnabled()) {\n//                log.warn(\"ApiDoc NOT Found: {}\", folderId);\n//            }\n//            return null;\n//        }\n//\n//        return doc;\n//    }\n\n    public String doc2Md(ApiDocObject doc) {\n        if (doc == null) {\n            return null;\n        }\n        ApiFolderObject folder = doc.getFolder();\n\n        try {\n            Map<String, Object> model = new HashMap<>();\n            model.put(\"folder\", folder);\n            model.put(\"doc\", doc);\n\n            if (api2DocProperties.isShowCurl()) {\n                String serverURL = api2DocProperties.getServerURL();\n                String curl = CurlBuilder.toCurl(doc, serverURL);\n                if (StringUtils.hasText(curl)) {\n                    model.put(\"curl\", curl);\n                }\n            }\n\n            Object mockResult = doc.toMockResult();\n            if (folder.isRestPack()) {\n                mockResult = HttpResult.successFully(mockResult);\n            }\n            if (mockResult != null) {\n                String resultJson = RestPackConfiguration.getObjectMapper()\n                        .writeValueAsString(mockResult);\n                if (StringUtils.hasText(resultJson)) {\n                    model.put(\"resultJson\", resultJson);\n                }\n            }\n\n            String folderId = folder.getId();\n            String upFirst = folderId.substring(0, 1).toUpperCase() + folderId.substring(1);\n            String folderClasses = upFirst + \"Service / \" + upFirst + \"Retrofit\";\n            model.put(\"folderClasses\", folderClasses);\n\n            String content = freeMarker.build(mdTemplate, model);\n            if (log.isInfoEnabled()) {\n                log.info(\"\\n{}\", content);\n            }\n            return content;\n        } catch (IOException | TemplateException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String loadMdFromResource(String path) throws Exception {\n        if (StringUtils.isEmpty(path)) {\n            throw new NullPointerException(\"path is null.\");\n        }\n        path = path.trim();\n        if (!path.startsWith(\"/\")) {\n            path = \"/\" + path;\n        }\n        path = \"api2doc\" + path;\n\n        ClassLoader loader = Thread.currentThread().getContextClassLoader();\n        String md = Strings.getResourceByPath(path, loader);\n        return md;\n    }\n\n    public String loadMdFromResource(String folderId, String docId) throws Exception {\n        ApiFolderObject folder = apiDocService.getFolder(folderId);\n        if (folder == null) {\n            log.warn(\"ApiFolder NOT Found: {}\", folderId);\n            return null;\n        }\n\n        Map<String, String> mds = folder.getMds();\n        if (mds == null || !mds.containsKey(docId)) {\n            log.warn(\"Markdown doc {} NOT Found in Folder: {}\", docId, folderId);\n            return null;\n        }\n\n        String fileName = mds.get(docId);\n        String path = folderId + \"/\" + fileName;\n        String md = loadMdFromResource(path);\n        return md;\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/FlexibleString.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\n/**\n * 将空串视为 null\n * 能转成 html 格式。\n */\npublic class FlexibleString {\n\n    private StringBuilder value = new StringBuilder();\n\n    public FlexibleString() {\n    }\n\n    public FlexibleString(String value) {\n        if (value != null) {\n            this.value.append(value);\n        }\n    }\n\n    public String getValue() {\n        if (value.length() == 0) {\n            return null;\n        }\n        return value.toString();\n    }\n\n    public void setValue(String value) {\n        if (value == null) {\n            this.value = new StringBuilder();\n        } else {\n            this.value = new StringBuilder(value);\n        }\n    }\n\n    public FlexibleString append(String appendValue) {\n        if (appendValue != null) {\n            value.append(appendValue);\n        }\n        return this;\n    }\n\n    public FlexibleString insertLine(String insertValue) {\n        if (insertValue != null) {\n            if (this.value.length() > 0) {\n                this.value.insert(0, insertValue + \"\\n\");\n            } else {\n                this.value.insert(0, insertValue);\n            }\n        }\n        return this;\n    }\n\n    public String html() {\n        if (value.length() == 0) {\n            return null;\n        }\n        String replacement = \"<br/>\";\n        return value.toString().replaceAll(\"\\n\", replacement);\n    }\n\n    public String javadoc(int indent) {\n        if (value.length() == 0) {\n            return null;\n        }\n        String replacement = \"\";\n        for (int i = 0; i < indent; i++) {\n            replacement += \"    \";\n        }\n        replacement = \"<br/>\\n\" + replacement + (\" * \");\n        return value.toString().replaceAll(\"\\n\", replacement);\n    }\n\n    @Override\n    public String toString() {\n        if (value.length() == 0) {\n            return \"\";\n        }\n        return value.toString();\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/impl/MappingMethod.java",
    "content": "package com.terran4j.commons.api2doc.impl;\n\nimport org.springframework.web.bind.annotation.*;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\nimport java.util.*;\n\npublic class MappingMethod {\n\n    /**\n     * 目前只支持这 5 种方法，其它的都用得太少，暂时不支持。\n     */\n    static final RequestMethod[] SUPPORT_METHODS = new RequestMethod[] {\n            RequestMethod.GET, RequestMethod.POST,\n            RequestMethod.PUT, RequestMethod.DELETE,\n            RequestMethod.PATCH\n    };\n\n    private final Method method;\n\n    private MappingMethod(Method method) {\n        this.method = method;\n    }\n\n    public Method getMethod() {\n        return method;\n    }\n\n    public String getName() {\n        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);\n        if (requestMapping != null) {\n            return requestMapping.name();\n        }\n\n        GetMapping getMapping = method.getAnnotation(GetMapping.class);\n        if (getMapping != null) {\n            return getMapping.name();\n        }\n\n        PostMapping postMapping = method.getAnnotation(PostMapping.class);\n        if (postMapping != null) {\n            return postMapping.name();\n        }\n\n        PutMapping putMapping = method.getAnnotation(PutMapping.class);\n        if (putMapping != null) {\n            return putMapping.name();\n        }\n\n        PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);\n        if (patchMapping != null) {\n            return patchMapping.name();\n        }\n\n        DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);\n        if (deleteMapping != null) {\n            return deleteMapping.name();\n        }\n\n        return null;\n    }\n\n    RequestMethod[] getRequestMethod() {\n        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);\n        if (requestMapping != null) {\n            RequestMethod[] methods = requestMapping.method();\n            if (methods == null || methods.length == 0) {\n                return SUPPORT_METHODS;\n            }\n            return methods;\n        }\n\n        GetMapping getMapping = method.getAnnotation(GetMapping.class);\n        if (getMapping != null) {\n            return new RequestMethod[]{RequestMethod.GET};\n        }\n\n        PostMapping postMapping = method.getAnnotation(PostMapping.class);\n        if (postMapping != null) {\n            return new RequestMethod[]{RequestMethod.POST};\n        }\n\n        PutMapping putMapping = method.getAnnotation(PutMapping.class);\n        if (putMapping != null) {\n            return new RequestMethod[]{RequestMethod.PUT};\n        }\n\n        PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);\n        if (patchMapping != null) {\n            return new RequestMethod[]{RequestMethod.PATCH};\n        }\n\n        DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);\n        if (deleteMapping != null) {\n            return new RequestMethod[]{RequestMethod.DELETE};\n        }\n\n        return SUPPORT_METHODS;\n    }\n\n    public String[] getPath() {\n        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);\n        if (requestMapping != null) {\n            return merge(requestMapping.path(), requestMapping.value());\n        }\n\n        GetMapping getMapping = method.getAnnotation(GetMapping.class);\n        if (getMapping != null) {\n            return merge(getMapping.path(), getMapping.value());\n        }\n\n        PostMapping postMapping = method.getAnnotation(PostMapping.class);\n        if (postMapping != null) {\n            return merge(postMapping.path(), postMapping.value());\n        }\n\n        PutMapping putMapping = method.getAnnotation(PutMapping.class);\n        if (putMapping != null) {\n            return merge(putMapping.path(), putMapping.value());\n        }\n\n        PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);\n        if (patchMapping != null) {\n            return merge(patchMapping.path(), patchMapping.value());\n        }\n\n        DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);\n        if (deleteMapping != null) {\n            return merge(deleteMapping.path(), deleteMapping.value());\n        }\n\n        return null;\n    }\n\n    private String[] merge(String[] strs1, String[] strs2) {\n        Set<String> strSet = new HashSet<>();\n        if (strs1 != null) {\n            strSet.addAll(Arrays.asList(strs1));\n        }\n        if (strs2 != null) {\n            strSet.addAll(Arrays.asList(strs2));\n        }\n\n        List<String> list = new ArrayList<>(strSet);\n        Collections.sort(list);\n        return list.toArray(new String[list.size()]);\n    }\n\n    public static List<MappingMethod> getMappingMethods(Class<?> clazz) {\n        List<MappingMethod> mappingMethods = new ArrayList<>();\n\n        Method[] methods = clazz.getDeclaredMethods();\n        if (methods == null || methods.length == 0) {\n            return mappingMethods;\n        }\n\n        for (Method method : methods) {\n            if (isMappingMethod(method)) {\n                MappingMethod mappingMethod = new MappingMethod(method);\n                mappingMethods.add(mappingMethod);\n            }\n        }\n\n        return mappingMethods;\n    }\n\n    public static boolean isMappingMethod(Method method) {\n        Annotation mapping = method.getAnnotation(RequestMapping.class);\n        if (mapping != null) {\n            return true;\n        }\n\n        mapping = method.getAnnotation(GetMapping.class);\n        if (mapping != null) {\n            return true;\n        }\n\n        mapping = method.getAnnotation(PostMapping.class);\n        if (mapping != null) {\n            return true;\n        }\n\n        mapping = method.getAnnotation(PutMapping.class);\n        if (mapping != null) {\n            return true;\n        }\n\n        mapping = method.getAnnotation(PatchMapping.class);\n        if (mapping != null) {\n            return true;\n        }\n\n        mapping = method.getAnnotation(DeleteMapping.class);\n        if (mapping != null) {\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/meta/ApiMetaService.java",
    "content": "package com.terran4j.commons.api2doc.meta;\n\nimport com.terran4j.commons.api2doc.controller.ApiEntry;\nimport com.terran4j.commons.api2doc.controller.ApiInfo;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.domain.ApiParamLocation;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Service\npublic class ApiMetaService {\n\n    @Autowired\n    private Api2DocService api2DocService;\n\n    public ApiInfo toApiInfo(String folderId, String docId) throws Exception {\n        ApiDocObject doc = api2DocService.getDocObject(folderId, docId);\n        ApiInfo apiInfo = new ApiInfo();\n        apiInfo.setUrl(doc.getPaths()[0]);\n        apiInfo.setDefaultMethod(doc.getMethods()[0].name());\n\n        List<ApiParamObject> apiParamObjects = doc.getParams();\n        if (apiParamObjects != null && apiParamObjects.size() > 0) {\n            List<ApiEntry> params = new ArrayList<>();\n            List<ApiEntry> headers = new ArrayList<>();\n            for (ApiParamObject apiParamObject : apiParamObjects) {\n                ApiEntry entry = new ApiEntry();\n                entry.setKey(apiParamObject.getId());\n                entry.setValue(apiParamObject.getSample().getValue());\n                ApiParamLocation paramLocation = apiParamObject.getLocation();\n                if (paramLocation == ApiParamLocation.RequestParam) {\n                    params.add(entry);\n                } else if (paramLocation == ApiParamLocation.RequestHeader) {\n                    headers.add(entry);\n                }\n            }\n            apiInfo.setParams(params);\n            apiInfo.setHeaders(headers);\n        }\n\n        return apiInfo;\n    }\n\n    public List<ClassMeta> toClassMetaList() {\n        List<ClassMeta> classes = new ArrayList<>();\n        List<ApiFolderObject> folderList = api2DocService.getFolders();\n        if (folderList == null || folderList.isEmpty()) {\n            return classes;\n        }\n\n        for(ApiFolderObject folder : folderList) {\n            ClassMeta classMeta = toClassMeta(folder);\n            classes.add(classMeta);\n        }\n\n        return classes;\n    }\n\n    public ClassMeta toClassMeta(ApiFolderObject folder) {\n        ClassMeta classMeta = new ClassMeta();\n        classMeta.setId(folder.getId());\n        classMeta.setName(folder.getName());\n        classMeta.setComment(folder.getComment().getValue());\n\n        List<ApiDocObject> docs = folder.getDocs();\n        if (docs != null && docs.size() > 0) {\n            for(ApiDocObject doc : docs) {\n                MethodMeta methodMeta = toMethodMeta(doc);\n                classMeta.addMethod(methodMeta);\n            }\n        }\n\n        return classMeta;\n    }\n\n    public MethodMeta toMethodMeta(ApiDocObject doc) {\n        MethodMeta methodMeta = new MethodMeta();\n        methodMeta.setId(doc.getId());\n        methodMeta.setComment(doc.getComment().getValue());\n        methodMeta.setName(doc.getName());\n        methodMeta.setPaths(doc.getPaths());\n        methodMeta.setRequestMethods(toRequestMethods(doc.getMethods()));\n\n        List<ApiParamObject> params = doc.getParams();\n        if (params != null && params.size() > 0) {\n            for (ApiParamObject param : params) {\n                ParamMeta paramMeta = toParamMeta(param);\n                methodMeta.addParam(paramMeta);\n            }\n        }\n\n        return methodMeta;\n    }\n\n    public ParamMeta toParamMeta(ApiParamObject param) {\n        ParamMeta paramMeta = new ParamMeta();\n        paramMeta.setId(param.getId());\n        paramMeta.setComment(param.getComment().getValue());\n        paramMeta.setDataType(param.getDataType().getName());\n        paramMeta.setLocation(param.getLocation().name());\n        paramMeta.setName(param.getName());\n        paramMeta.setRequired(param.isRequired());\n        return paramMeta;\n    }\n\n    private String[] toRequestMethods(RequestMethod[] methods) {\n        if (methods == null || methods.length == 0) {\n            return new String[]{};\n        }\n\n        String[] results = new String[methods.length];\n        for (int i = 0; i < methods.length; i++) {\n            RequestMethod method = methods[i];\n            results[i] = method.name();\n        }\n        return results;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/meta/ClassMeta.java",
    "content": "package com.terran4j.commons.api2doc.meta;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ClassMeta {\n\n    private String id;\n\n    private String name;\n\n    private String comment;\n\n    private final List<MethodMeta> methods = new ArrayList<>();\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getComment() {\n        return comment;\n    }\n\n    public void setComment(String comment) {\n        this.comment = comment;\n    }\n\n    public List<MethodMeta> getMethods() {\n        return methods;\n    }\n\n    public void addMethod(MethodMeta method) {\n        this.methods.add(method);\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/meta/MethodMeta.java",
    "content": "package com.terran4j.commons.api2doc.meta;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class MethodMeta {\n\n    private String id;\n\n    private String name;\n\n    private String comment;\n\n    private String[] paths;\n\n    private String[] requestMethods;\n\n    private final List<ParamMeta> params = new ArrayList<>();\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getComment() {\n        return comment;\n    }\n\n    public void setComment(String comment) {\n        this.comment = comment;\n    }\n\n    public String[] getPaths() {\n        return paths;\n    }\n\n    public void setPaths(String[] paths) {\n        this.paths = paths;\n    }\n\n    public String[] getRequestMethods() {\n        return requestMethods;\n    }\n\n    public void setRequestMethods(String[] requestMethods) {\n        this.requestMethods = requestMethods;\n    }\n\n    public List<ParamMeta> getParams() {\n        return params;\n    }\n\n    public void addParam(ParamMeta param) {\n        this.params.add(param);\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/java/com/terran4j/commons/api2doc/meta/ParamMeta.java",
    "content": "package com.terran4j.commons.api2doc.meta;\n\nimport com.terran4j.commons.api2doc.domain.ApiDataType;\nimport com.terran4j.commons.api2doc.domain.ApiParamLocation;\n\npublic class ParamMeta {\n\n    private String id;\n\n    private String name;\n\n    private String comment;\n\n    private boolean required;\n\n    private String dataType;\n\n    private String location;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getComment() {\n        return comment;\n    }\n\n    public void setComment(String comment) {\n        this.comment = comment;\n    }\n\n    public boolean isRequired() {\n        return required;\n    }\n\n    public void setRequired(boolean required) {\n        this.required = required;\n    }\n\n    public String getDataType() {\n        return dataType;\n    }\n\n    public void setDataType(String dataType) {\n        this.dataType = dataType;\n    }\n\n    public String getLocation() {\n        return location;\n    }\n\n    public void setLocation(String location) {\n        this.location = location;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/main/resources/api2doc/welcome.md",
    "content": "\n欢迎使用 Api2Doc ！\n"
  },
  {
    "path": "commons-api2doc/src/main/resources/com/terran4j/commons/api2doc/codewriter/bean.java.ftl",
    "content": "<#if config.pkgName??>\npackage ${config.pkgName};\n</#if>\n\n<#list imports as import>\nimport ${import};\n</#list>\n\n/**\n<#if comment??>\n * ${comment}<br/>\n</#if>\n * \n<#if config.declaredComment??>\n * ${config.declaredComment}\n</#if>\n */\npublic class ${class} {\n\n<#list fields as field>\n    <#if field.comment ??>\n    /**\n     * ${field.comment}\n     */\n    </#if>\n    private ${field.type} ${field.name};\n\n</#list>\n\n<#list fields as field>\n    <#if field.comment ??>\n    /**\n     * @return ${field.comment}\n     */\n    </#if>\n\tpublic ${field.type} ${field.getMethod}() {\n\t\treturn ${field.name};\n\t}\n\n\tpublic void ${field.setMethod}(${field.type} ${field.name}) {\n\t\tthis.${field.name} = ${field.name};\n\t}\n\n</#list>\n}"
  },
  {
    "path": "commons-api2doc/src/main/resources/com/terran4j/commons/api2doc/codewriter/enum.java.ftl",
    "content": "<#if config.pkgName??>\npackage ${config.pkgName};\n</#if>\n\n/**\n<#if config.declaredComment??>\n * ${config.declaredComment}\n</#if>\n */\npublic enum ${class} {\n\n<#list enums as enum>\n<#if enum.comment??>\n\t/**\n\t * ${enum.comment}\n\t */\n</#if>\n\t${enum.name},\n\n</#list>\n\n}"
  },
  {
    "path": "commons-api2doc/src/main/resources/com/terran4j/commons/api2doc/codewriter/retrofit.java.ftl",
    "content": "<#if config.pkgName??>\npackage ${config.pkgName};\n</#if>\n\nimport retrofit2.Call;\nimport retrofit2.http.*;\n\n<#list imports as import>\nimport ${import};\n</#list>\n\n/**\n<#if comment??>\n * ${comment!}\n</#if>\n * \n<#if config.declaredComment??>\n * ${config.declaredComment}\n</#if>\n */\npublic interface ${class} {\n\n<#list methods as method>\n    /**\n<#if method.comment??>\n     * ${method.comment}\n</#if>\n<#if method.params??>\n<#list method.params as param>\n     * @param ${param.id} ${param.comment!}\n</#list>\n</#if>\n<#if method.returnClass ??>\n     * @return 返回由 ${method.returnClass} 对象序列化而成的 json 串。\n</#if>\n     */\n<#if method.annos??>\n<#list method.annos as anno>\n    ${anno}\n</#list>\n</#if>\n    Call<String> ${method.name}(<#list method.params as param>${param.expression}</#list>);\n\n</#list>\n}"
  },
  {
    "path": "commons-api2doc/src/main/resources/com/terran4j/commons/api2doc/impl/doc.md.ftl",
    "content": "**API标识**\n- ${folder.id}-${doc.id}\n\n<#if doc.comment?? && doc.comment!=\"\">\n**API简介** \n- ${doc.comment}\n</#if>\n\n<br/>\n\n<#--**客户端API类**-->\n<#--- ${folderClasses}-->\n\n<#--**客户端API方法**-->\n<#--- ${doc.id}-->\n\n<#--<br/>-->\n\n**请求URI**\n<#list doc.paths as path>\n- `${path}`\n</#list>\n\n**请求方法**\n<#list doc.methods as method>\n- `${method}`\n</#list>\n\n<#if doc.params?? && (doc.params?size > 0) >\n**请求参数**\n\n| 参数名 | 是否必须 | 参数形式 | 数据类型 | 说明    | 示例值 |\n|:-----    |-----         |-----        |-----         |------   |-----     |\n<#list doc.params as param>\n| ${param.id} | ${param.requiredName} | ${param.location} | ${param.typeName} | ${param.comment.html()!} | ${param.sample.html()!} |\n</#list>\n</#if>\n\n<#if curl??>\n**请求示例（curl 命令格式）**\n\n```\n${curl}\n```\n</#if>\n\n<br/>\n\n<#if resultJson??>\n\n**返回数据示例**\n\n```json\n${resultJson}\n```\n</#if>\n\n<#if doc.results?? && (doc.results?size > 0) >\n<!-- 复杂数据类型的情况 -->\n\n**返回数据说明**\n- [${doc.returnTypeDesc}](#${doc.results[0].groupId}) （参见下面的类型说明）\n\n<!-- 所有的子类型的列表说明 -->\n<#list doc.results as result>\n\n<!-- 给子类型定义一个锚，好点到这里。 -->\n<#if result.groupId??>\n<span id=\"${result.groupId}\"><br /></span>\n</#if>\n\n**${result.groupName!}类型说明：**\n\n| 字段名 | 类型   | 说明 | 示例值 |\n|:---- |----- |-----  |----- |\n<#list result.children as item>\n| ${item.id} | <#if item.refGroupId??>[${item.typeName}](#${item.refGroupId})<#else>${item.typeName}</#if> | ${item.comment.html()!} | ${item.sample.html()!} |\n</#list>\n</#list>\n\n<#elseif doc.resultType?? >\n<!-- 简单数据类型的情况。 -->\n\n**返回数据说明**\n\n| 类型  | 说明 | 示例值 |\n|:----   |-----  |-----     |\n|  ${doc.resultType.typeName} |  ${doc.resultType.comment.html()!} | ${doc.resultType.sample.html()!} |\n</#if>\n\n<br/>\n\n<#if doc.errors?? && (doc.errors?size > 0) >\n**错误码**\n\n| 错误码  | 说明   |\n|:----      |-----   |\n<#list doc.errors as error>\n| ${error.id} | ${error.comment.html()!} |\n</#list>\n</#if>\n"
  },
  {
    "path": "commons-api2doc/src/main/resources/static/api2doc/css/doc.less",
    "content": "//////////////////////////    整个 html、 body 及公共的样式   /////////////////\nhtml {\n  overflow-y: scroll;\n}\n\n@media only screen and (min-width: 48rem) {\n  body {\n    font-size: 1.4rem;\n  }\n}\n\n@media only screen and (min-width: 76.8rem) {\n  body {\n    font-size: 1.6rem;\n  }\n}\n\n////////////////////////////    文档标题及内容块的样式    ///////////////////////\n\n.title {\n  color: rgb(51, 51, 51);\n  font-family: \"Microsoft Yahei, 微软雅黑, Tahoma, Arial, Helvetica, STHeiti\";\n  font-size: 2.4rem;\n  font-weight: bold;\n  text-align: left;\n  text-rendering: optimizeLegibility;\n  text-size-adjust: 100%;\n  height: 4rem;\n  line-height: 4rem;\n  margin-left: 3rem;\n  padding-bottom: 0.5rem;\n  padding-top: 1rem;\n}\n\n.content {\n  padding-top: 1rem;\n  word-break: break-all;\n  overflow-x: hidden;\n  overflow-y: hidden;\n  font-size: 1.6rem;\n  line-height: 1.7rem;\n  margin-left: 3rem;\n}\n\n.test-entry {\n  float: right;\n  margin-top: 1rem;\n  margin-right: 3rem;\n}\n\n.clear {\n  clear: both;\n}\n\n//////////////////////////////   自定义滚动条样式  /////////////////\n::-webkit-scrollbar { /* 整个滚条背景 */\n  width: 0.5rem;\n  height: 0.5rem;\n}\n\n::-webkit-scrollbar-thumb { // 滚动条滑块（是主要部分）\n  background-color: #c2c2c2; /* 滚条内嵌颜色 */\n  background-clip: padding-box;\n  min-height: 3rem;\n}\n\n::-webkit-scrollbar-track { //  外层轨道。\n  background-color: #FFF;\n}\n\n::-webkit-scrollbar-button { // 不要两端的按钮。\n  display: none;\n}"
  },
  {
    "path": "commons-api2doc/src/main/resources/static/api2doc/css/home.less",
    "content": "\n// 菜单栏背景的颜色。\n@menu-color: white;\n\n// 分隔线的颜色：\n@split-color: rgba(0,0,0,.07);\n\nhtml {\n  overflow: hidden;\n}\n\n.doc-app {\n  width: 99.52%;\n  margin: 0px;\n  padding: 0px;\n}\n\n.doc-top {\n  width: 100%;\n  height: 5rem;\n  border-bottom: 1px solid rgba(0,0,0,.07);\n}\n\n.doc-icon {\n  float: left;\n  position: relative;\n  text-align: center;\n  margin-left: 2rem;\n  margin-top: 0.7rem;\n  background-position: center center;\n}\n\n.doc-icon-img {\n  width: 3rem;\n  height: 3rem;\n  cursor: pointer;\n}\n\n.doc-title {\n  float: left;\n  position: relative;\n  font-size: 2rem;\n  font-weight: bold;\n  line-height: 4rem;\n  height: 4rem;\n  margin-left: 2rem;\n  text-align: center;\n  vertical-align: middle;\n  overflow: hidden;\n}\n\n.doc-body, .doc-left, .doc-middle {\n}\n\n.doc-body {\n  width: 100%;\n  position: absolute;\n  top: 5rem;\n  bottom: 0px;\n  left: 0px;\n  margin-top: 0.673rem;\n}\n\n.doc-left {\n  width: 20%;\n  height: 100%;\n  overflow-x: hidden;\n  overflow-y: auto;\n  float: left;\n  position: relative;\n  display: block;\n  background-color: @menu-color;\n}\n\n#doc-menus {\n  width: 100%;\n  margin-top: 1rem;\n}\n\n.doc-content {\n  width: 79%;\n  height: 100%;\n  float: left;\n  overflow: hidden;\n  border-left: 1px solid @split-color;\n}\n\n.doc-frame {\n  width: 100%;\n  height: 100%;\n}\n\n.doc-end {\n  clear: both;\n  display: none;\n}\n\n//////////////////////  重新定义菜单栏的颜色 ////////////////////\n\n.el-menu {\n  background-color: @menu-color;\n}\n\n.el-submenu .el-menu {\n  background-color: @menu-color;\n}\n\n//////////////////////////////   自定义滚动条样式  /////////////////\n::-webkit-scrollbar { /* 整个滚条背景 */\n  width: 0.5rem;\n  height: 0.5rem;\n}\n\n::-webkit-scrollbar-thumb { // 滚动条滑块（是主要部分）\n  background-color: #c2c2c2; /* 滚条内嵌颜色 */\n  background-clip: padding-box;\n  min-height: 3rem;\n}\n\n::-webkit-scrollbar-track { //  外层轨道。\n  background-color: #FFF;\n}\n\n::-webkit-scrollbar-button { // 不要两端的按钮。\n  display: none;\n}"
  },
  {
    "path": "commons-api2doc/src/main/resources/static/api2doc/css/md.less",
    "content": "\n////////////////////////////////    通用样式    //////////////////////\n\np, blockquote, ul, ol, dl, li, table, pre {\n  margin: 1.5rem 0;\n}\n\n////////////////////////////////   各种型号的标题的样式  ////////////////////\n\nh1, h2, h3, h4, h5, h6 {\n  font-weight: bold;\n  padding-bottom: 1rem;\n}\n\nh1 {\n  color: #000000;\n  font-size: 2.8rem;\n}\n\nh2 {\n  color: #000000;\n  font-size: 2.4rem;\n}\n\nh3 {\n  font-size: 1.8rem;\n}\n\nh4 {\n  font-size: 1.6rem;\n}\n\nh5 {\n  font-size: 1.4rem;\n}\n\nh6 {\n  color: #777777;\n  background-color: inherit;\n  font-size: 1.4rem;\n}\n\nhr {\n  height: 0.2em;\n  border: 0;\n  color: #CCCCCC;\n  background-color: #CCCCCC;\n}\n\n//////////////////////////////     段落的样式      //////////////////////\np {\n  margin: 1rem 0;\n}\n\n//////////////////////////////     代码块的样式      //////////////////////\npre {\n  overflow: auto;\n  width: 94.5%;\n  padding: 0.5rem;\n  white-space: pre-wrap; /*css-3*/\n  white-space: -moz-pre-wrap; /*Mozilla,since1999*/\n  white-space: -o-pre-wrap; /*Opera7*/\n  word-wrap: break-word; /*InternetExplorer5.5+*/\n}\n\ncode {\n  font-family: Consolas, Monaco, Andale Mono, monospace;\n  background-color: #F8F8F8;\n  border: 1px solid #CCCCCC;\n  border-radius: 0.3rem;\n  padding: 0 0.2em;\n  line-height: 1;\n  color: #d14;\n  white-space: pre-wrap;\n}\n\n///////////////////////////    链接的样式   ////////////////////////////////////\na {\n  color: #0645ad;\n  text-decoration: none;\n}\n\na:visited {\n  color: #0b0080;\n}\n\na:hover {\n  color: #06e;\n}\n\na:active {\n  color: #faa700;\n}\n\na:focus {\n  outline: thin dotted;\n}\n\na:hover, a:active {\n  outline: 0;\n}\n\n/////////////////////////////   被选中的文本的样式   ///////////////////////\n::-moz-selection {\n  background: rgba(255, 255, 0, 0.3);\n  color: #000\n}\n\n::selection {\n  background: rgba(255, 255, 0, 0.3);\n  color: #000\n}\n\na::-moz-selection {\n  background: rgba(255, 255, 0, 0.3);\n  color: #0645ad\n}\n\na::selection {\n  background: rgba(255, 255, 0, 0.3);\n  color: #0645ad\n}\n\n///////////////////////////////    条目的样式    /////////////////////\nblockquote {\n  color: #666666;\n  margin: 0;\n  padding-left: 3em;\n  border-left: 0.5em #EEE solid;\n}\n\nul, ol {\n  margin: 1em 0;\n  padding: 0 0 0 2em;\n}\n\nli p:last-child {\n  margin: 0\n}\n\ndd {\n  margin: 0 0 0 2em;\n}\n\n/////////////////////////   图片的样式   ////////////////////////\nimg {\n  border: 0;\n  -ms-interpolation-mode: bicubic;\n  vertical-align: middle;\n  max-width: 100%;\n}\n\n/////////////////////////   表格的样式   ////////////////////////\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n  width: 95%;\n}\n\ntr {\n  color: rgb(255, 255, 255);\n}\n\nth, td {\n  vertical-align: top;\n  display: table-cell;\n  padding: 0.8rem;\n  line-height: 2rem;\n  text-align: left;\n  vertical-align: top;\n  color: rgb(51, 51, 51);\n  border-top: 1px solid #ddd;\n  border-left: 1px solid #ddd;\n}\n\nthead tr {\n  background-color: rgb(0, 136, 204);\n}\n\nthead tr th {\n  text-align: left;\n  min-width: 7.7rem;\n  vertical-align: bottom;\n  font-weight: bold;\n}\n\ntbody {\n  display: table-row-group;\n  vertical-align: middle;\n  border-color: inherit;\n  border-bottom: 1px solid #ddd;\n  border-right: 1px solid #ddd;\n}\n\ntbody tr th {\n  text-align: left;\n  border-bottom-left-radius: 0.4rem;\n  color: #333;\n}"
  },
  {
    "path": "commons-api2doc/src/main/resources/static/api2doc/css/test.less",
    "content": "@left-space: 1rem;\n@font-base-size: 1rem;\n\nh2 {\n  margin-left: @left-space;\n}\n\n//h3 {\n//  margin-left: @left-space;\n//}\n\n.clear {\n  clear: both;\n}\n\n//////////////////////////////////////////////////////////////////\n\n.items {\n  margin-top: 2rem;\n  margin-left: @left-space;\n}\n\n.items-desc {\n  font-size: @font-base-size;\n  margin-left: @left-space;\n  margin-bottom: 0.5rem;\n}\n\n.item-list {\n  float: left;\n  width: 95%;\n}\n\n.item {\n  margin-bottom: 0.5rem;\n  margin-right: 2rem;\n  width: 45%;\n  float: left;\n}\n\n.item-key {\n  width: 25%;\n  margin-left: @left-space;\n  float: left;\n}\n\n.item-split {\n  margin-top: 0.7rem;\n  margin-left: 0.3rem;\n  font-size: @font-base-size;\n  float: left;\n}\n\n.item-value {\n  width: 65%;\n  margin-left: 0.1rem;\n  float: left;\n}\n\n.item-new {\n  margin-left: @left-space;\n  float: left;\n  clear: both;\n}\n\n////////////////////////////////////////////////////////////////////////\n\n.request {\n  width: 95%;\n  margin-left: @left-space;\n}\n\n.request-method {\n  width: 10%;\n  margin-left: @left-space;\n  float: left;\n}\n\n.request-url {\n  width: 77.5%;\n  margin-left: 1rem;\n  float: left;\n}\n\n//////////////////////////////////////////////////////////////////\n\n.send {\n  margin-left: @left-space;\n  width: 95%;\n}\n\n.send-button {\n  float: right;\n}\n\n/////////////////////////////////////////////////////////////////////////\n\n.headers {\n  margin-top: 2rem;\n  margin-left: @left-space;\n}\n\n//////////////////////////////////////////////////////////////////////////\n\n.response {\n  margin-left: @left-space;\n  margin-top: 4rem;\n  width: 95%;\n}\n\n.response-code {\n  margin-left: @left-space;\n}\n\n///////////////////////////////////////////////////////////////////////\n\n.output {\n  margin-left: @left-space;\n  width: 95%;\n}\n\npre.output-logs {\n  margin-left: @left-space;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n  background-color: #F0F0F0;\n}"
  },
  {
    "path": "commons-api2doc/src/main/resources/static/api2doc/flexible-lite/flexible-lite-1.0.js",
    "content": "/**\n * @param designWidth:  设计稿的实际宽度值，需要根据实际设置\n */\nfunction flex(designWidth) {\n    var doc = document,\n        win = window,\n        docEl = doc.documentElement,\n        remStyle = document.createElement(\"style\"),\n        tid; // setTimeout 的句柄。\n\n    function refreshRem() {\n        var width = docEl.getBoundingClientRect().width;\n        var rem = width * 10 / designWidth;\n        remStyle.innerHTML = 'html{font-size:' + rem + 'px;}';\n    }\n\n    if (docEl.firstElementChild) {\n        docEl.firstElementChild.appendChild(remStyle);\n    } else {\n        var wrap = doc.createElement(\"div\");\n        wrap.appendChild(remStyle);\n        doc.write(wrap.innerHTML);\n        wrap = null;\n    }\n    //要等 viewport 设置好后才能执行 refreshRem，不然 refreshRem 会执行2次；\n    refreshRem();\n\n    win.addEventListener(\"resize\", function () {\n        clearTimeout(tid); //防止执行两次\n        tid = setTimeout(refreshRem, 300);\n    }, false);\n\n    win.addEventListener(\"pageshow\", function (e) {\n        if (e.persisted) { // 浏览器后退的时候重新计算\n            clearTimeout(tid);\n            tid = setTimeout(refreshRem, 300);\n        }\n    }, false);\n\n    if (doc.readyState === \"complete\") {\n        doc.body.style.fontSize = \"16px\";\n    } else {\n        doc.addEventListener(\"DOMContentLoaded\", function (e) {\n            doc.body.style.fontSize = \"16px\";\n        }, false);\n    }\n}"
  },
  {
    "path": "commons-api2doc/src/main/resources/static/api2doc/less/less-1.7.0.js",
    "content": "/*! \n * LESS - Leaner CSS v1.7.0 \n * http://lesscss.org \n * \n * Copyright (c) 2009-2014, Alexis Sellier <self@cloudhead.net> \n * Licensed under the Apache v2 License. \n * \n */ \n\n /** * @license Apache v2\n */ \n\n\n\n(function (window, undefined) {//\n// Stub out `require` in the browser\n//\nfunction require(arg) {\n    return window.less[arg.split('/')[1]];\n};\n\n\nif (typeof(window.less) === 'undefined' || typeof(window.less.nodeType) !== 'undefined') { window.less = {}; }\nless = window.less;\ntree = window.less.tree = {};\nless.mode = 'browser';\n\nvar less, tree;\n\n// Node.js does not have a header file added which defines less\nif (less === undefined) {\n    less = exports;\n    tree = require('./tree');\n    less.mode = 'node';\n}\n//\n// less.js - parser\n//\n//    A relatively straight-forward predictive parser.\n//    There is no tokenization/lexing stage, the input is parsed\n//    in one sweep.\n//\n//    To make the parser fast enough to run in the browser, several\n//    optimization had to be made:\n//\n//    - Matching and slicing on a huge input is often cause of slowdowns.\n//      The solution is to chunkify the input into smaller strings.\n//      The chunks are stored in the `chunks` var,\n//      `j` holds the current chunk index, and `currentPos` holds\n//      the index of the current chunk in relation to `input`.\n//      This gives us an almost 4x speed-up.\n//\n//    - In many cases, we don't need to match individual tokens;\n//      for example, if a value doesn't hold any variables, operations\n//      or dynamic references, the parser can effectively 'skip' it,\n//      treating it as a literal.\n//      An example would be '1px solid #000' - which evaluates to itself,\n//      we don't need to know what the individual components are.\n//      The drawback, of course is that you don't get the benefits of\n//      syntax-checking on the CSS. This gives us a 50% speed-up in the parser,\n//      and a smaller speed-up in the code-gen.\n//\n//\n//    Token matching is done with the `$` function, which either takes\n//    a terminal string or regexp, or a non-terminal function to call.\n//    It also takes care of moving all the indices forwards.\n//\n//\nless.Parser = function Parser(env) {\n    var input,       // LeSS input string\n        i,           // current index in `input`\n        j,           // current chunk\n        saveStack = [],   // holds state for backtracking\n        furthest,    // furthest index the parser has gone to\n        chunks,      // chunkified input\n        current,     // current chunk\n        currentPos,  // index of current chunk, in `input`\n        parser,\n        parsers,\n        rootFilename = env && env.filename;\n\n    // Top parser on an import tree must be sure there is one \"env\"\n    // which will then be passed around by reference.\n    if (!(env instanceof tree.parseEnv)) {\n        env = new tree.parseEnv(env);\n    }\n\n    var imports = this.imports = {\n        paths: env.paths || [],  // Search paths, when importing\n        queue: [],               // Files which haven't been imported yet\n        files: env.files,        // Holds the imported parse trees\n        contents: env.contents,  // Holds the imported file contents\n        contentsIgnoredChars: env.contentsIgnoredChars, // lines inserted, not in the original less\n        mime:  env.mime,         // MIME type of .less files\n        error: null,             // Error in parsing/evaluating an import\n        push: function (path, currentFileInfo, importOptions, callback) {\n            var parserImports = this;\n            this.queue.push(path);\n\n            var fileParsedFunc = function (e, root, fullPath) {\n                parserImports.queue.splice(parserImports.queue.indexOf(path), 1); // Remove the path from the queue\n\n                var importedPreviously = fullPath === rootFilename;\n\n                parserImports.files[fullPath] = root;                        // Store the root\n\n                if (e && !parserImports.error) { parserImports.error = e; }\n\n                callback(e, root, importedPreviously, fullPath);\n            };\n\n            if (less.Parser.importer) {\n                less.Parser.importer(path, currentFileInfo, fileParsedFunc, env);\n            } else {\n                less.Parser.fileLoader(path, currentFileInfo, function(e, contents, fullPath, newFileInfo) {\n                    if (e) {fileParsedFunc(e); return;}\n\n                    var newEnv = new tree.parseEnv(env);\n\n                    newEnv.currentFileInfo = newFileInfo;\n                    newEnv.processImports = false;\n                    newEnv.contents[fullPath] = contents;\n\n                    if (currentFileInfo.reference || importOptions.reference) {\n                        newFileInfo.reference = true;\n                    }\n\n                    if (importOptions.inline) {\n                        fileParsedFunc(null, contents, fullPath);\n                    } else {\n                        new(less.Parser)(newEnv).parse(contents, function (e, root) {\n                            fileParsedFunc(e, root, fullPath);\n                        });\n                    }\n                }, env);\n            }\n        }\n    };\n\n    function save()    { currentPos = i; saveStack.push( { current: current, i: i, j: j }); }\n    function restore() { var state = saveStack.pop(); current = state.current; currentPos = i = state.i; j = state.j; }\n    function forget() { saveStack.pop(); }\n\n    function sync() {\n        if (i > currentPos) {\n            current = current.slice(i - currentPos);\n            currentPos = i;\n        }\n    }\n    function isWhitespace(str, pos) {\n        var code = str.charCodeAt(pos | 0);\n        return (code <= 32) && (code === 32 || code === 10 || code === 9);\n    }\n    //\n    // Parse from a token, regexp or string, and move forward if match\n    //\n    function $(tok) {\n        var tokType = typeof tok,\n            match, length;\n\n        // Either match a single character in the input,\n        // or match a regexp in the current chunk (`current`).\n        //\n        if (tokType === \"string\") {\n            if (input.charAt(i) !== tok) {\n                return null;\n            }\n            skipWhitespace(1);\n            return tok;\n        }\n\n        // regexp\n        sync ();\n        if (! (match = tok.exec(current))) {\n            return null;\n        }\n\n        length = match[0].length;\n\n        // The match is confirmed, add the match length to `i`,\n        // and consume any extra white-space characters (' ' || '\\n')\n        // which come after that. The reason for this is that LeSS's\n        // grammar is mostly white-space insensitive.\n        //\n        skipWhitespace(length);\n\n        if(typeof(match) === 'string') {\n            return match;\n        } else {\n            return match.length === 1 ? match[0] : match;\n        }\n    }\n\n    // Specialization of $(tok)\n    function $re(tok) {\n        if (i > currentPos) {\n            current = current.slice(i - currentPos);\n            currentPos = i;\n        }\n        var m = tok.exec(current);\n        if (!m) {\n            return null;\n        }\n\n        skipWhitespace(m[0].length);\n        if(typeof m === \"string\") {\n            return m;\n        }\n\n        return m.length === 1 ? m[0] : m;\n    }\n\n    var _$re = $re;\n\n    // Specialization of $(tok)\n    function $char(tok) {\n        if (input.charAt(i) !== tok) {\n            return null;\n        }\n        skipWhitespace(1);\n        return tok;\n    }\n\n    function skipWhitespace(length) {\n        var oldi = i, oldj = j,\n            curr = i - currentPos,\n            endIndex = i + current.length - curr,\n            mem = (i += length),\n            inp = input,\n            c;\n\n        for (; i < endIndex; i++) {\n            c = inp.charCodeAt(i);\n            if (c > 32) {\n                break;\n            }\n\n            if ((c !== 32) && (c !== 10) && (c !== 9) && (c !== 13)) {\n                break;\n            }\n         }\n\n        current = current.slice(length + i - mem + curr);\n        currentPos = i;\n\n        if (!current.length && (j < chunks.length - 1)) {\n            current = chunks[++j];\n            skipWhitespace(0); // skip space at the beginning of a chunk\n            return true; // things changed\n        }\n\n        return oldi !== i || oldj !== j;\n    }\n\n    function expect(arg, msg) {\n        // some older browsers return typeof 'function' for RegExp\n        var result = (Object.prototype.toString.call(arg) === '[object Function]') ? arg.call(parsers) : $(arg);\n        if (result) {\n            return result;\n        }\n        error(msg || (typeof(arg) === 'string' ? \"expected '\" + arg + \"' got '\" + input.charAt(i) + \"'\"\n                                               : \"unexpected token\"));\n    }\n\n    // Specialization of expect()\n    function expectChar(arg, msg) {\n        if (input.charAt(i) === arg) {\n            skipWhitespace(1);\n            return arg;\n        }\n        error(msg || \"expected '\" + arg + \"' got '\" + input.charAt(i) + \"'\");\n    }\n\n    function error(msg, type) {\n        var e = new Error(msg);\n        e.index = i;\n        e.type = type || 'Syntax';\n        throw e;\n    }\n\n    // Same as $(), but don't change the state of the parser,\n    // just return the match.\n    function peek(tok) {\n        if (typeof(tok) === 'string') {\n            return input.charAt(i) === tok;\n        } else {\n            return tok.test(current);\n        }\n    }\n\n    // Specialization of peek()\n    function peekChar(tok) {\n        return input.charAt(i) === tok;\n    }\n\n\n    function getInput(e, env) {\n        if (e.filename && env.currentFileInfo.filename && (e.filename !== env.currentFileInfo.filename)) {\n            return parser.imports.contents[e.filename];\n        } else {\n            return input;\n        }\n    }\n\n    function getLocation(index, inputStream) {\n        var n = index + 1,\n            line = null,\n            column = -1;\n\n        while (--n >= 0 && inputStream.charAt(n) !== '\\n') {\n            column++;\n        }\n\n        if (typeof index === 'number') {\n            line = (inputStream.slice(0, index).match(/\\n/g) || \"\").length;\n        }\n\n        return {\n            line: line,\n            column: column\n        };\n    }\n\n    function getDebugInfo(index, inputStream, env) {\n        var filename = env.currentFileInfo.filename;\n        if(less.mode !== 'browser' && less.mode !== 'rhino') {\n            filename = require('path').resolve(filename);\n        }\n\n        return {\n            lineNumber: getLocation(index, inputStream).line + 1,\n            fileName: filename\n        };\n    }\n\n    function LessError(e, env) {\n        var input = getInput(e, env),\n            loc = getLocation(e.index, input),\n            line = loc.line,\n            col  = loc.column,\n            callLine = e.call && getLocation(e.call, input).line,\n            lines = input.split('\\n');\n\n        this.type = e.type || 'Syntax';\n        this.message = e.message;\n        this.filename = e.filename || env.currentFileInfo.filename;\n        this.index = e.index;\n        this.line = typeof(line) === 'number' ? line + 1 : null;\n        this.callLine = callLine + 1;\n        this.callExtract = lines[callLine];\n        this.stack = e.stack;\n        this.column = col;\n        this.extract = [\n            lines[line - 1],\n            lines[line],\n            lines[line + 1]\n        ];\n    }\n\n    LessError.prototype = new Error();\n    LessError.prototype.constructor = LessError;\n\n    this.env = env = env || {};\n\n    // The optimization level dictates the thoroughness of the parser,\n    // the lower the number, the less nodes it will create in the tree.\n    // This could matter for debugging, or if you want to access\n    // the individual nodes in the tree.\n    this.optimization = ('optimization' in this.env) ? this.env.optimization : 1;\n\n    //\n    // The Parser\n    //\n    parser = {\n\n        imports: imports,\n        //\n        // Parse an input string into an abstract syntax tree,\n        // @param str A string containing 'less' markup\n        // @param callback call `callback` when done.\n        // @param [additionalData] An optional map which can contains vars - a map (key, value) of variables to apply\n        //\n        parse: function (str, callback, additionalData) {\n            var root, line, lines, error = null, globalVars, modifyVars, preText = \"\";\n\n            i = j = currentPos = furthest = 0;\n\n            globalVars = (additionalData && additionalData.globalVars) ? less.Parser.serializeVars(additionalData.globalVars) + '\\n' : '';\n            modifyVars = (additionalData && additionalData.modifyVars) ? '\\n' + less.Parser.serializeVars(additionalData.modifyVars) : '';\n\n            if (globalVars || (additionalData && additionalData.banner)) {\n                preText = ((additionalData && additionalData.banner) ? additionalData.banner : \"\") + globalVars;\n                parser.imports.contentsIgnoredChars[env.currentFileInfo.filename] = preText.length;\n            }\n\n            str = str.replace(/\\r\\n/g, '\\n');\n            // Remove potential UTF Byte Order Mark\n            input = str = preText + str.replace(/^\\uFEFF/, '') + modifyVars;\n            parser.imports.contents[env.currentFileInfo.filename] = str;\n\n            // Split the input into chunks.\n            chunks = (function (input) {\n                var len = input.length, level = 0, parenLevel = 0,\n                    lastOpening, lastOpeningParen, lastMultiComment, lastMultiCommentEndBrace,\n                    chunks = [], emitFrom = 0,\n                    parserCurrentIndex, currentChunkStartIndex, cc, cc2, matched;\n\n                function fail(msg, index) {\n                    error = new(LessError)({\n                        index: index || parserCurrentIndex,\n                        type: 'Parse',\n                        message: msg,\n                        filename: env.currentFileInfo.filename\n                    }, env);\n                }\n\n                function emitChunk(force) {\n                    var len = parserCurrentIndex - emitFrom;\n                    if (((len < 512) && !force) || !len) {\n                        return;\n                    }\n                    chunks.push(input.slice(emitFrom, parserCurrentIndex + 1));\n                    emitFrom = parserCurrentIndex + 1;\n                }\n\n                for (parserCurrentIndex = 0; parserCurrentIndex < len; parserCurrentIndex++) {\n                    cc = input.charCodeAt(parserCurrentIndex);\n                    if (((cc >= 97) && (cc <= 122)) || (cc < 34)) {\n                        // a-z or whitespace\n                        continue;\n                    }\n\n                    switch (cc) {\n                        case 40:                        // (\n                            parenLevel++; \n                            lastOpeningParen = parserCurrentIndex; \n                            continue;\n                        case 41:                        // )\n                            if (--parenLevel < 0) {\n                                return fail(\"missing opening `(`\");\n                            }\n                            continue;\n                        case 59:                        // ;\n                            if (!parenLevel) { emitChunk(); }\n                            continue;\n                        case 123:                       // {\n                            level++; \n                            lastOpening = parserCurrentIndex; \n                            continue;\n                        case 125:                       // }\n                            if (--level < 0) {\n                                return fail(\"missing opening `{`\");\n                            }\n                            if (!level && !parenLevel) { emitChunk(); }\n                            continue;\n                        case 92:                        // \\\n                            if (parserCurrentIndex < len - 1) { parserCurrentIndex++; continue; }\n                            return fail(\"unescaped `\\\\`\");\n                        case 34:\n                        case 39:\n                        case 96:                        // \", ' and `\n                            matched = 0;\n                            currentChunkStartIndex = parserCurrentIndex;\n                            for (parserCurrentIndex = parserCurrentIndex + 1; parserCurrentIndex < len; parserCurrentIndex++) {\n                                cc2 = input.charCodeAt(parserCurrentIndex);\n                                if (cc2 > 96) { continue; }\n                                if (cc2 == cc) { matched = 1; break; }\n                                if (cc2 == 92) {        // \\\n                                    if (parserCurrentIndex == len - 1) {\n                                        return fail(\"unescaped `\\\\`\");\n                                    }\n                                    parserCurrentIndex++;\n                                }\n                            }\n                            if (matched) { continue; }\n                            return fail(\"unmatched `\" + String.fromCharCode(cc) + \"`\", currentChunkStartIndex);\n                        case 47:                        // /, check for comment\n                            if (parenLevel || (parserCurrentIndex == len - 1)) { continue; }\n                            cc2 = input.charCodeAt(parserCurrentIndex + 1);\n                            if (cc2 == 47) {\n                                // //, find lnfeed\n                                for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len; parserCurrentIndex++) {\n                                    cc2 = input.charCodeAt(parserCurrentIndex);\n                                    if ((cc2 <= 13) && ((cc2 == 10) || (cc2 == 13))) { break; }\n                                }\n                            } else if (cc2 == 42) {\n                                // /*, find */\n                                lastMultiComment = currentChunkStartIndex = parserCurrentIndex;\n                                for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len - 1; parserCurrentIndex++) {\n                                    cc2 = input.charCodeAt(parserCurrentIndex);\n                                    if (cc2 == 125) { lastMultiCommentEndBrace = parserCurrentIndex; }\n                                    if (cc2 != 42) { continue; }\n                                    if (input.charCodeAt(parserCurrentIndex + 1) == 47) { break; }\n                                }\n                                if (parserCurrentIndex == len - 1) {\n                                    return fail(\"missing closing `*/`\", currentChunkStartIndex);\n                                }\n                                parserCurrentIndex++;\n                            }\n                            continue;\n                        case 42:                       // *, check for unmatched */\n                            if ((parserCurrentIndex < len - 1) && (input.charCodeAt(parserCurrentIndex + 1) == 47)) {\n                                return fail(\"unmatched `/*`\");\n                            }\n                            continue;\n                    }\n                }\n\n                if (level !== 0) {\n                    if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) {\n                        return fail(\"missing closing `}` or `*/`\", lastOpening);\n                    } else {\n                        return fail(\"missing closing `}`\", lastOpening);\n                    }\n                } else if (parenLevel !== 0) {\n                    return fail(\"missing closing `)`\", lastOpeningParen);\n                }\n\n                emitChunk(true);\n                return chunks;\n            })(str);\n\n            if (error) {\n                return callback(new(LessError)(error, env));\n            }\n\n            current = chunks[0];\n\n            // Start with the primary rule.\n            // The whole syntax tree is held under a Ruleset node,\n            // with the `root` property set to true, so no `{}` are\n            // output. The callback is called when the input is parsed.\n            try {\n                root = new(tree.Ruleset)(null, this.parsers.primary());\n                root.root = true;\n                root.firstRoot = true;\n            } catch (e) {\n                return callback(new(LessError)(e, env));\n            }\n\n            root.toCSS = (function (evaluate) {\n                return function (options, variables) {\n                    options = options || {};\n                    var evaldRoot,\n                        css,\n                        evalEnv = new tree.evalEnv(options);\n                        \n                    //\n                    // Allows setting variables with a hash, so:\n                    //\n                    //   `{ color: new(tree.Color)('#f01') }` will become:\n                    //\n                    //   new(tree.Rule)('@color',\n                    //     new(tree.Value)([\n                    //       new(tree.Expression)([\n                    //         new(tree.Color)('#f01')\n                    //       ])\n                    //     ])\n                    //   )\n                    //\n                    if (typeof(variables) === 'object' && !Array.isArray(variables)) {\n                        variables = Object.keys(variables).map(function (k) {\n                            var value = variables[k];\n\n                            if (! (value instanceof tree.Value)) {\n                                if (! (value instanceof tree.Expression)) {\n                                    value = new(tree.Expression)([value]);\n                                }\n                                value = new(tree.Value)([value]);\n                            }\n                            return new(tree.Rule)('@' + k, value, false, null, 0);\n                        });\n                        evalEnv.frames = [new(tree.Ruleset)(null, variables)];\n                    }\n\n                    try {\n                        var preEvalVisitors = [],\n                            visitors = [\n                                new(tree.joinSelectorVisitor)(),\n                                new(tree.processExtendsVisitor)(),\n                                new(tree.toCSSVisitor)({compress: Boolean(options.compress)})\n                            ], i, root = this;\n\n                        if (options.plugins) {\n                            for(i =0; i < options.plugins.length; i++) {\n                                if (options.plugins[i].isPreEvalVisitor) {\n                                    preEvalVisitors.push(options.plugins[i]);\n                                } else {\n                                    if (options.plugins[i].isPreVisitor) {\n                                        visitors.splice(0, 0, options.plugins[i]);\n                                    } else {\n                                        visitors.push(options.plugins[i]);\n                                    }\n                                }\n                            }\n                        }\n\n                        for(i = 0; i < preEvalVisitors.length; i++) {\n                            preEvalVisitors[i].run(root);\n                        }\n\n                        evaldRoot = evaluate.call(root, evalEnv);\n\n                        for(i = 0; i < visitors.length; i++) {\n                            visitors[i].run(evaldRoot);\n                        }\n\n                        if (options.sourceMap) {\n                            evaldRoot = new tree.sourceMapOutput(\n                                {\n                                    contentsIgnoredCharsMap: parser.imports.contentsIgnoredChars,\n                                    writeSourceMap: options.writeSourceMap,\n                                    rootNode: evaldRoot,\n                                    contentsMap: parser.imports.contents,\n                                    sourceMapFilename: options.sourceMapFilename,\n                                    sourceMapURL: options.sourceMapURL,\n                                    outputFilename: options.sourceMapOutputFilename,\n                                    sourceMapBasepath: options.sourceMapBasepath,\n                                    sourceMapRootpath: options.sourceMapRootpath,\n                                    outputSourceFiles: options.outputSourceFiles,\n                                    sourceMapGenerator: options.sourceMapGenerator\n                                });\n                        }\n\n                        css = evaldRoot.toCSS({\n                                compress: Boolean(options.compress),\n                                dumpLineNumbers: env.dumpLineNumbers,\n                                strictUnits: Boolean(options.strictUnits),\n                                numPrecision: 8});\n                    } catch (e) {\n                        throw new(LessError)(e, env);\n                    }\n\n                    if (options.cleancss && less.mode === 'node') {\n                        var CleanCSS = require('clean-css'),\n                            cleancssOptions = options.cleancssOptions || {};\n\n                        if (cleancssOptions.keepSpecialComments === undefined) {\n                            cleancssOptions.keepSpecialComments = \"*\";\n                        }\n                        cleancssOptions.processImport = false;\n                        cleancssOptions.noRebase = true;\n                        if (cleancssOptions.noAdvanced === undefined) {\n                            cleancssOptions.noAdvanced = true;\n                        }\n\n                        return new CleanCSS(cleancssOptions).minify(css);\n                    } else if (options.compress) {\n                        return css.replace(/(^(\\s)+)|((\\s)+$)/g, \"\");\n                    } else {\n                        return css;\n                    }\n                };\n            })(root.eval);\n\n            // If `i` is smaller than the `input.length - 1`,\n            // it means the parser wasn't able to parse the whole\n            // string, so we've got a parsing error.\n            //\n            // We try to extract a \\n delimited string,\n            // showing the line where the parse error occured.\n            // We split it up into two parts (the part which parsed,\n            // and the part which didn't), so we can color them differently.\n            if (i < input.length - 1) {\n                i = furthest;\n                var loc = getLocation(i, input);\n                lines = input.split('\\n');\n                line = loc.line + 1;\n\n                error = {\n                    type: \"Parse\",\n                    message: \"Unrecognised input\",\n                    index: i,\n                    filename: env.currentFileInfo.filename,\n                    line: line,\n                    column: loc.column,\n                    extract: [\n                        lines[line - 2],\n                        lines[line - 1],\n                        lines[line]\n                    ]\n                };\n            }\n\n            var finish = function (e) {\n                e = error || e || parser.imports.error;\n\n                if (e) {\n                    if (!(e instanceof LessError)) {\n                        e = new(LessError)(e, env);\n                    }\n\n                    return callback(e);\n                }\n                else {\n                    return callback(null, root);\n                }\n            };\n\n            if (env.processImports !== false) {\n                new tree.importVisitor(this.imports, finish)\n                    .run(root);\n            } else {\n                return finish();\n            }\n        },\n\n        //\n        // Here in, the parsing rules/functions\n        //\n        // The basic structure of the syntax tree generated is as follows:\n        //\n        //   Ruleset ->  Rule -> Value -> Expression -> Entity\n        //\n        // Here's some LESS code:\n        //\n        //    .class {\n        //      color: #fff;\n        //      border: 1px solid #000;\n        //      width: @w + 4px;\n        //      > .child {...}\n        //    }\n        //\n        // And here's what the parse tree might look like:\n        //\n        //     Ruleset (Selector '.class', [\n        //         Rule (\"color\",  Value ([Expression [Color #fff]]))\n        //         Rule (\"border\", Value ([Expression [Dimension 1px][Keyword \"solid\"][Color #000]]))\n        //         Rule (\"width\",  Value ([Expression [Operation \"+\" [Variable \"@w\"][Dimension 4px]]]))\n        //         Ruleset (Selector [Element '>', '.child'], [...])\n        //     ])\n        //\n        //  In general, most rules will try to parse a token with the `$()` function, and if the return\n        //  value is truly, will return a new node, of the relevant type. Sometimes, we need to check\n        //  first, before parsing, that's when we use `peek()`.\n        //\n        parsers: parsers = {\n            //\n            // The `primary` rule is the *entry* and *exit* point of the parser.\n            // The rules here can appear at any level of the parse tree.\n            //\n            // The recursive nature of the grammar is an interplay between the `block`\n            // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,\n            // as represented by this simplified grammar:\n            //\n            //     primary  →  (ruleset | rule)+\n            //     ruleset  →  selector+ block\n            //     block    →  '{' primary '}'\n            //\n            // Only at one point is the primary rule not called from the\n            // block rule: at the root level.\n            //\n            primary: function () {\n                var mixin = this.mixin, $re = _$re, root = [], node;\n\n                while (current)\n                {\n                    node = this.extendRule() || mixin.definition() || this.rule() || this.ruleset() ||\n                        mixin.call() || this.comment() || this.rulesetCall() || this.directive();\n                    if (node) {\n                        root.push(node);\n                    } else {\n                        if (!($re(/^[\\s\\n]+/) || $re(/^;+/))) {\n                            break;\n                        }\n                    }\n                    if (peekChar('}')) {\n                        break;\n                    }\n                }\n\n                return root;\n            },\n\n            // We create a Comment node for CSS comments `/* */`,\n            // but keep the LeSS comments `//` silent, by just skipping\n            // over them.\n            comment: function () {\n                var comment;\n\n                if (input.charAt(i) !== '/') { return; }\n\n                if (input.charAt(i + 1) === '/') {\n                    return new(tree.Comment)($re(/^\\/\\/.*/), true, i, env.currentFileInfo);\n                }\n                comment = $re(/^\\/\\*(?:[^*]|\\*+[^\\/*])*\\*+\\/\\n?/);\n                if (comment) {\n                    return new(tree.Comment)(comment, false, i, env.currentFileInfo);\n                }\n            },\n\n            comments: function () {\n                var comment, comments = [];\n\n                while(true) {\n                    comment = this.comment();\n                    if (!comment) {\n                        break;\n                    }\n                    comments.push(comment);\n                }\n\n                return comments;\n            },\n\n            //\n            // Entities are tokens which can be found inside an Expression\n            //\n            entities: {\n                //\n                // A string, which supports escaping \" and '\n                //\n                //     \"milky way\" 'he\\'s the one!'\n                //\n                quoted: function () {\n                    var str, j = i, e, index = i;\n\n                    if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings\n                    if (input.charAt(j) !== '\"' && input.charAt(j) !== \"'\") { return; }\n\n                    if (e) { $char('~'); }\n\n                    str = $re(/^\"((?:[^\"\\\\\\r\\n]|\\\\.)*)\"|'((?:[^'\\\\\\r\\n]|\\\\.)*)'/);\n                    if (str) {\n                        return new(tree.Quoted)(str[0], str[1] || str[2], e, index, env.currentFileInfo);\n                    }\n                },\n\n                //\n                // A catch-all word, such as:\n                //\n                //     black border-collapse\n                //\n                keyword: function () {\n                    var k;\n\n                    k = $re(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/);\n                    if (k) {\n                        var color = tree.Color.fromKeyword(k);\n                        if (color) {\n                            return color;\n                        }\n                        return new(tree.Keyword)(k);\n                    }\n                },\n\n                //\n                // A function call\n                //\n                //     rgb(255, 0, 255)\n                //\n                // We also try to catch IE's `alpha()`, but let the `alpha` parser\n                // deal with the details.\n                //\n                // The arguments are parsed with the `entities.arguments` parser.\n                //\n                call: function () {\n                    var name, nameLC, args, alpha_ret, index = i;\n\n                    name = /^([\\w-]+|%|progid:[\\w\\.]+)\\(/.exec(current);\n                    if (!name) { return; }\n\n                    name = name[1];\n                    nameLC = name.toLowerCase();\n                    if (nameLC === 'url') {\n                        return null;\n                    }\n\n                    i += name.length;\n\n                    if (nameLC === 'alpha') {\n                        alpha_ret = parsers.alpha();\n                        if(typeof alpha_ret !== 'undefined') {\n                            return alpha_ret;\n                        }\n                    }\n\n                    $char('('); // Parse the '(' and consume whitespace.\n\n                    args = this.arguments();\n\n                    if (! $char(')')) {\n                        return;\n                    }\n\n                    if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); }\n                },\n                arguments: function () {\n                    var args = [], arg;\n\n                    while (true) {\n                        arg = this.assignment() || parsers.expression();\n                        if (!arg) {\n                            break;\n                        }\n                        args.push(arg);\n                        if (! $char(',')) {\n                            break;\n                        }\n                    }\n                    return args;\n                },\n                literal: function () {\n                    return this.dimension() ||\n                           this.color() ||\n                           this.quoted() ||\n                           this.unicodeDescriptor();\n                },\n\n                // Assignments are argument entities for calls.\n                // They are present in ie filter properties as shown below.\n                //\n                //     filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )\n                //\n\n                assignment: function () {\n                    var key, value;\n                    key = $re(/^\\w+(?=\\s?=)/i);\n                    if (!key) {\n                        return;\n                    }\n                    if (!$char('=')) {\n                        return;\n                    }\n                    value = parsers.entity();\n                    if (value) {\n                        return new(tree.Assignment)(key, value);\n                    }\n                },\n\n                //\n                // Parse url() tokens\n                //\n                // We use a specific rule for urls, because they don't really behave like\n                // standard function calls. The difference is that the argument doesn't have\n                // to be enclosed within a string, so it can't be parsed as an Expression.\n                //\n                url: function () {\n                    var value;\n\n                    if (input.charAt(i) !== 'u' || !$re(/^url\\(/)) {\n                        return;\n                    }\n\n                    value = this.quoted() || this.variable() ||\n                            $re(/^(?:(?:\\\\[\\(\\)'\"])|[^\\(\\)'\"])+/) || \"\";\n\n                    expectChar(')');\n\n                    return new(tree.URL)((value.value != null || value instanceof tree.Variable)\n                                        ? value : new(tree.Anonymous)(value), env.currentFileInfo);\n                },\n\n                //\n                // A Variable entity, such as `@fink`, in\n                //\n                //     width: @fink + 2px\n                //\n                // We use a different parser for variable definitions,\n                // see `parsers.variable`.\n                //\n                variable: function () {\n                    var name, index = i;\n\n                    if (input.charAt(i) === '@' && (name = $re(/^@@?[\\w-]+/))) {\n                        return new(tree.Variable)(name, index, env.currentFileInfo);\n                    }\n                },\n\n                // A variable entity useing the protective {} e.g. @{var}\n                variableCurly: function () {\n                    var curly, index = i;\n\n                    if (input.charAt(i) === '@' && (curly = $re(/^@\\{([\\w-]+)\\}/))) {\n                        return new(tree.Variable)(\"@\" + curly[1], index, env.currentFileInfo);\n                    }\n                },\n\n                //\n                // A Hexadecimal color\n                //\n                //     #4F3C2F\n                //\n                // `rgb` and `hsl` colors are parsed through the `entities.call` parser.\n                //\n                color: function () {\n                    var rgb;\n\n                    if (input.charAt(i) === '#' && (rgb = $re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {\n                        return new(tree.Color)(rgb[1]);\n                    }\n                },\n\n                //\n                // A Dimension, that is, a number and a unit\n                //\n                //     0.5em 95%\n                //\n                dimension: function () {\n                    var value, c = input.charCodeAt(i);\n                    //Is the first char of the dimension 0-9, '.', '+' or '-'\n                    if ((c > 57 || c < 43) || c === 47 || c == 44) {\n                        return;\n                    }\n\n                    value = $re(/^([+-]?\\d*\\.?\\d+)(%|[a-z]+)?/);\n                    if (value) {\n                        return new(tree.Dimension)(value[1], value[2]);\n                    }\n                },\n\n                //\n                // A unicode descriptor, as is used in unicode-range\n                //\n                // U+0??  or U+00A1-00A9\n                //\n                unicodeDescriptor: function () {\n                    var ud;\n\n                    ud = $re(/^U\\+[0-9a-fA-F?]+(\\-[0-9a-fA-F?]+)?/);\n                    if (ud) {\n                        return new(tree.UnicodeDescriptor)(ud[0]);\n                    }\n                },\n\n                //\n                // JavaScript code to be evaluated\n                //\n                //     `window.location.href`\n                //\n                javascript: function () {\n                    var str, j = i, e;\n\n                    if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings\n                    if (input.charAt(j) !== '`') { return; }\n                    if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) {\n                        error(\"You are using JavaScript, which has been disabled.\");\n                    }\n\n                    if (e) { $char('~'); }\n\n                    str = $re(/^`([^`]*)`/);\n                    if (str) {\n                        return new(tree.JavaScript)(str[1], i, e);\n                    }\n                }\n            },\n\n            //\n            // The variable part of a variable definition. Used in the `rule` parser\n            //\n            //     @fink:\n            //\n            variable: function () {\n                var name;\n\n                if (input.charAt(i) === '@' && (name = $re(/^(@[\\w-]+)\\s*:/))) { return name[1]; }\n            },\n\n            //\n            // The variable part of a variable definition. Used in the `rule` parser\n            //\n            //     @fink();\n            //\n            rulesetCall: function () {\n                var name;\n\n                if (input.charAt(i) === '@' && (name = $re(/^(@[\\w-]+)\\s*\\(\\s*\\)\\s*;/))) { \n                    return new tree.RulesetCall(name[1]); \n                }\n            },\n\n            //\n            // extend syntax - used to extend selectors\n            //\n            extend: function(isRule) {\n                var elements, e, index = i, option, extendList, extend;\n\n                if (!(isRule ? $re(/^&:extend\\(/) : $re(/^:extend\\(/))) { return; }\n\n                do {\n                    option = null;\n                    elements = null;\n                    while (! (option = $re(/^(all)(?=\\s*(\\)|,))/))) {\n                        e = this.element();\n                        if (!e) { break; }\n                        if (elements) { elements.push(e); } else { elements = [ e ]; }\n                    }\n\n                    option = option && option[1];\n\n                    extend = new(tree.Extend)(new(tree.Selector)(elements), option, index);\n                    if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }\n\n                } while($char(\",\"));\n                \n                expect(/^\\)/);\n\n                if (isRule) {\n                    expect(/^;/);\n                }\n\n                return extendList;\n            },\n\n            //\n            // extendRule - used in a rule to extend all the parent selectors\n            //\n            extendRule: function() {\n                return this.extend(true);\n            },\n            \n            //\n            // Mixins\n            //\n            mixin: {\n                //\n                // A Mixin call, with an optional argument list\n                //\n                //     #mixins > .square(#fff);\n                //     .rounded(4px, black);\n                //     .button;\n                //\n                // The `while` loop is there because mixins can be\n                // namespaced, but we only support the child and descendant\n                // selector for now.\n                //\n                call: function () {\n                    var s = input.charAt(i), important = false, index = i, elemIndex,\n                        elements, elem, e, c, args;\n\n                    if (s !== '.' && s !== '#') { return; }\n\n                    save(); // stop us absorbing part of an invalid selector\n\n                    while (true) {\n                        elemIndex = i;\n                        e = $re(/^[#.](?:[\\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/);\n                        if (!e) {\n                            break;\n                        }\n                        elem = new(tree.Element)(c, e, elemIndex, env.currentFileInfo);\n                        if (elements) { elements.push(elem); } else { elements = [ elem ]; }\n                        c = $char('>');\n                    }\n\n                    if (elements) {\n                        if ($char('(')) {\n                            args = this.args(true).args;\n                            expectChar(')');\n                        }\n\n                        if (parsers.important()) {\n                            important = true;\n                        }\n\n                        if (parsers.end()) {\n                            forget();\n                            return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important);\n                        }\n                    }\n\n                    restore();\n                },\n                args: function (isCall) {\n                    var parsers = parser.parsers, entities = parsers.entities,\n                        returner = { args:null, variadic: false },\n                        expressions = [], argsSemiColon = [], argsComma = [],\n                        isSemiColonSeperated, expressionContainsNamed, name, nameLoop, value, arg;\n\n                    save();\n\n                    while (true) {\n                        if (isCall) {\n                            arg = parsers.detachedRuleset() || parsers.expression();\n                        } else {\n                            parsers.comments();\n                            if (input.charAt(i) === '.' && $re(/^\\.{3}/)) {\n                                returner.variadic = true;\n                                if ($char(\";\") && !isSemiColonSeperated) {\n                                    isSemiColonSeperated = true;\n                                }\n                                (isSemiColonSeperated ? argsSemiColon : argsComma)\n                                    .push({ variadic: true });\n                                break;\n                            }\n                            arg = entities.variable() || entities.literal() || entities.keyword();\n                        }\n\n                        if (!arg) {\n                            break;\n                        }\n\n                        nameLoop = null;\n                        if (arg.throwAwayComments) {\n                            arg.throwAwayComments();\n                        }\n                        value = arg;\n                        var val = null;\n\n                        if (isCall) {\n                            // Variable\n                            if (arg.value && arg.value.length == 1) {\n                                val = arg.value[0];\n                            }\n                        } else {\n                            val = arg;\n                        }\n\n                        if (val && val instanceof tree.Variable) {\n                            if ($char(':')) {\n                                if (expressions.length > 0) {\n                                    if (isSemiColonSeperated) {\n                                        error(\"Cannot mix ; and , as delimiter types\");\n                                    }\n                                    expressionContainsNamed = true;\n                                }\n\n                                // we do not support setting a ruleset as a default variable - it doesn't make sense\n                                // However if we do want to add it, there is nothing blocking it, just don't error\n                                // and remove isCall dependency below\n                                value = (isCall && parsers.detachedRuleset()) || parsers.expression();\n\n                                if (!value) {\n                                    if (isCall) {\n                                        error(\"could not understand value for named argument\");\n                                    } else {\n                                        restore();\n                                        returner.args = [];\n                                        return returner;\n                                    }\n                                }\n                                nameLoop = (name = val.name);\n                            } else if (!isCall && $re(/^\\.{3}/)) {\n                                returner.variadic = true;\n                                if ($char(\";\") && !isSemiColonSeperated) {\n                                    isSemiColonSeperated = true;\n                                }\n                                (isSemiColonSeperated ? argsSemiColon : argsComma)\n                                    .push({ name: arg.name, variadic: true });\n                                break;\n                            } else if (!isCall) {\n                                name = nameLoop = val.name;\n                                value = null;\n                            }\n                        }\n\n                        if (value) {\n                            expressions.push(value);\n                        }\n\n                        argsComma.push({ name:nameLoop, value:value });\n\n                        if ($char(',')) {\n                            continue;\n                        }\n\n                        if ($char(';') || isSemiColonSeperated) {\n\n                            if (expressionContainsNamed) {\n                                error(\"Cannot mix ; and , as delimiter types\");\n                            }\n\n                            isSemiColonSeperated = true;\n\n                            if (expressions.length > 1) {\n                                value = new(tree.Value)(expressions);\n                            }\n                            argsSemiColon.push({ name:name, value:value });\n\n                            name = null;\n                            expressions = [];\n                            expressionContainsNamed = false;\n                        }\n                    }\n\n                    forget();\n                    returner.args = isSemiColonSeperated ? argsSemiColon : argsComma;\n                    return returner;\n                },\n                //\n                // A Mixin definition, with a list of parameters\n                //\n                //     .rounded (@radius: 2px, @color) {\n                //        ...\n                //     }\n                //\n                // Until we have a finer grained state-machine, we have to\n                // do a look-ahead, to make sure we don't have a mixin call.\n                // See the `rule` function for more information.\n                //\n                // We start by matching `.rounded (`, and then proceed on to\n                // the argument list, which has optional default values.\n                // We store the parameters in `params`, with a `value` key,\n                // if there is a value, such as in the case of `@radius`.\n                //\n                // Once we've got our params list, and a closing `)`, we parse\n                // the `{...}` block.\n                //\n                definition: function () {\n                    var name, params = [], match, ruleset, cond, variadic = false;\n                    if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') ||\n                        peek(/^[^{]*\\}/)) {\n                        return;\n                    }\n\n                    save();\n\n                    match = $re(/^([#.](?:[\\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\\s*\\(/);\n                    if (match) {\n                        name = match[1];\n\n                        var argInfo = this.args(false);\n                        params = argInfo.args;\n                        variadic = argInfo.variadic;\n\n                        // .mixincall(\"@{a}\");\n                        // looks a bit like a mixin definition.. \n                        // also\n                        // .mixincall(@a: {rule: set;});\n                        // so we have to be nice and restore\n                        if (!$char(')')) {\n                            furthest = i;\n                            restore();\n                            return;\n                        }\n                        \n                        parsers.comments();\n\n                        if ($re(/^when/)) { // Guard\n                            cond = expect(parsers.conditions, 'expected condition');\n                        }\n\n                        ruleset = parsers.block();\n\n                        if (ruleset) {\n                            forget();\n                            return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);\n                        } else {\n                            restore();\n                        }\n                    } else {\n                        forget();\n                    }\n                }\n            },\n\n            //\n            // Entities are the smallest recognized token,\n            // and can be found inside a rule's value.\n            //\n            entity: function () {\n                var entities = this.entities;\n\n                return entities.literal() || entities.variable() || entities.url() ||\n                       entities.call()    || entities.keyword()  || entities.javascript() ||\n                       this.comment();\n            },\n\n            //\n            // A Rule terminator. Note that we use `peek()` to check for '}',\n            // because the `block` rule will be expecting it, but we still need to make sure\n            // it's there, if ';' was ommitted.\n            //\n            end: function () {\n                return $char(';') || peekChar('}');\n            },\n\n            //\n            // IE's alpha function\n            //\n            //     alpha(opacity=88)\n            //\n            alpha: function () {\n                var value;\n\n                if (! $re(/^\\(opacity=/i)) { return; }\n                value = $re(/^\\d+/) || this.entities.variable();\n                if (value) {\n                    expectChar(')');\n                    return new(tree.Alpha)(value);\n                }\n            },\n\n            //\n            // A Selector Element\n            //\n            //     div\n            //     + h1\n            //     #socks\n            //     input[type=\"text\"]\n            //\n            // Elements are the building blocks for Selectors,\n            // they are made out of a `Combinator` (see combinator rule),\n            // and an element name, such as a tag a class, or `*`.\n            //\n            element: function () {\n                var e, c, v, index = i;\n\n                c = this.combinator();\n\n                e = $re(/^(?:\\d+\\.\\d+|\\d+)%/) || $re(/^(?:[.#]?|:*)(?:[\\w-]|[^\\x00-\\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||\n                    $char('*') || $char('&') || this.attribute() || $re(/^\\([^()@]+\\)/) || $re(/^[\\.#](?=@)/) ||\n                    this.entities.variableCurly();\n\n                if (! e) {\n                    save();\n                    if ($char('(')) {\n                        if ((v = this.selector()) && $char(')')) {\n                            e = new(tree.Paren)(v);\n                            forget();\n                        } else {\n                            restore();\n                        }\n                    } else {\n                        forget();\n                    }\n                }\n\n                if (e) { return new(tree.Element)(c, e, index, env.currentFileInfo); }\n            },\n\n            //\n            // Combinators combine elements together, in a Selector.\n            //\n            // Because our parser isn't white-space sensitive, special care\n            // has to be taken, when parsing the descendant combinator, ` `,\n            // as it's an empty space. We have to check the previous character\n            // in the input, to see if it's a ` ` character. More info on how\n            // we deal with this in *combinator.js*.\n            //\n            combinator: function () {\n                var c = input.charAt(i);\n                \n                if (c === '>' || c === '+' || c === '~' || c === '|' || c === '^') {\n                    i++;\n                    if (input.charAt(i) === '^') {\n                        c = '^^';\n                        i++;\n                    }\n                    while (isWhitespace(input, i)) { i++; }\n                    return new(tree.Combinator)(c);\n                } else if (isWhitespace(input, i - 1)) {\n                    return new(tree.Combinator)(\" \");\n                } else {\n                    return new(tree.Combinator)(null);\n                }\n            },\n            //\n            // A CSS selector (see selector below)\n            // with less extensions e.g. the ability to extend and guard\n            //\n            lessSelector: function () {\n                return this.selector(true);\n            },\n            //\n            // A CSS Selector\n            //\n            //     .class > div + h1\n            //     li a:hover\n            //\n            // Selectors are made out of one or more Elements, see above.\n            //\n            selector: function (isLess) {\n                var index = i, $re = _$re, elements, extendList, c, e, extend, when, condition;\n\n                while ((isLess && (extend = this.extend())) || (isLess && (when = $re(/^when/))) || (e = this.element())) {\n                    if (when) {\n                        condition = expect(this.conditions, 'expected condition');\n                    } else if (condition) {\n                        error(\"CSS guard can only be used at the end of selector\");\n                    } else if (extend) {\n                        if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }\n                    } else {\n                        if (extendList) { error(\"Extend can only be used at the end of selector\"); }\n                        c = input.charAt(i);\n                        if (elements) { elements.push(e); } else { elements = [ e ]; }\n                        e = null;\n                    }\n                    if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') {\n                        break;\n                    }\n                }\n\n                if (elements) { return new(tree.Selector)(elements, extendList, condition, index, env.currentFileInfo); }\n                if (extendList) { error(\"Extend must be used to extend a selector, it cannot be used on its own\"); }\n            },\n            attribute: function () {\n                if (! $char('[')) { return; }\n\n                var entities = this.entities,\n                    key, val, op;\n\n                if (!(key = entities.variableCurly())) {\n                    key = expect(/^(?:[_A-Za-z0-9-\\*]*\\|)?(?:[_A-Za-z0-9-]|\\\\.)+/);\n                }\n\n                op = $re(/^[|~*$^]?=/);\n                if (op) {\n                    val = entities.quoted() || $re(/^[0-9]+%/) || $re(/^[\\w-]+/) || entities.variableCurly();\n                }\n\n                expectChar(']');\n\n                return new(tree.Attribute)(key, op, val);\n            },\n\n            //\n            // The `block` rule is used by `ruleset` and `mixin.definition`.\n            // It's a wrapper around the `primary` rule, with added `{}`.\n            //\n            block: function () {\n                var content;\n                if ($char('{') && (content = this.primary()) && $char('}')) {\n                    return content;\n                }\n            },\n\n            blockRuleset: function() {\n                var block = this.block();\n\n                if (block) {\n                    block = new tree.Ruleset(null, block);\n                }\n                return block;\n            },\n            \n            detachedRuleset: function() {\n                var blockRuleset = this.blockRuleset();\n                if (blockRuleset) {\n                    return new tree.DetachedRuleset(blockRuleset);\n                }\n            },\n\n            //\n            // div, .class, body > p {...}\n            //\n            ruleset: function () {\n                var selectors, s, rules, debugInfo;\n                \n                save();\n\n                if (env.dumpLineNumbers) {\n                    debugInfo = getDebugInfo(i, input, env);\n                }\n\n                while (true) {\n                    s = this.lessSelector();\n                    if (!s) {\n                        break;\n                    }\n                    if (selectors) { selectors.push(s); } else { selectors = [ s ]; }\n                    this.comments();\n                    if (s.condition && selectors.length > 1) {\n                        error(\"Guards are only currently allowed on a single selector.\");\n                    }\n                    if (! $char(',')) { break; }\n                    if (s.condition) {\n                        error(\"Guards are only currently allowed on a single selector.\");\n                    }\n                    this.comments();\n                }\n\n                if (selectors && (rules = this.block())) {\n                    forget();\n                    var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports);\n                    if (env.dumpLineNumbers) {\n                        ruleset.debugInfo = debugInfo;\n                    }\n                    return ruleset;\n                } else {\n                    // Backtrack\n                    furthest = i;\n                    restore();\n                }\n            },\n            rule: function (tryAnonymous) {\n                var name, value, startOfRule = i, c = input.charAt(startOfRule), important, merge, isVariable;\n\n                if (c === '.' || c === '#' || c === '&') { return; }\n\n                save();\n\n                name = this.variable() || this.ruleProperty();\n                if (name) {\n                    isVariable = typeof name === \"string\";\n                    \n                    if (isVariable) {\n                        value = this.detachedRuleset();\n                    }\n                    \n                    if (!value) {\n                        // prefer to try to parse first if its a variable or we are compressing\n                        // but always fallback on the other one\n                        value = !tryAnonymous && (env.compress || isVariable) ?\n                            (this.value() || this.anonymousValue()) :\n                            (this.anonymousValue() || this.value());\n    \n                        important = this.important();\n                        \n                        // a name returned by this.ruleProperty() is always an array of the form:\n                        // [string-1, ..., string-n, \"\"] or [string-1, ..., string-n, \"+\"]\n                        // where each item is a tree.Keyword or tree.Variable\n                        merge = !isVariable && name.pop().value;\n                    }\n\n                    if (value && this.end()) {\n                        forget();\n                        return new (tree.Rule)(name, value, important, merge, startOfRule, env.currentFileInfo);\n                    } else {\n                        furthest = i;\n                        restore();\n                        if (value && !tryAnonymous) {\n                            return this.rule(true);\n                        }\n                    }\n                } else {\n                    forget();\n                }\n            },\n            anonymousValue: function () {\n                var match;\n                match = /^([^@+\\/'\"*`(;{}-]*);/.exec(current);\n                if (match) {\n                    i += match[0].length - 1;\n                    return new(tree.Anonymous)(match[1]);\n                }\n            },\n\n            //\n            // An @import directive\n            //\n            //     @import \"lib\";\n            //\n            // Depending on our environemnt, importing is done differently:\n            // In the browser, it's an XHR request, in Node, it would be a\n            // file-system operation. The function used for importing is\n            // stored in `import`, which we pass to the Import constructor.\n            //\n            \"import\": function () {\n                var path, features, index = i;\n\n                save();\n\n                var dir = $re(/^@import?\\s+/);\n\n                var options = (dir ? this.importOptions() : null) || {};\n\n                if (dir && (path = this.entities.quoted() || this.entities.url())) {\n                    features = this.mediaFeatures();\n                    if ($char(';')) {\n                        forget();\n                        features = features && new(tree.Value)(features);\n                        return new(tree.Import)(path, features, options, index, env.currentFileInfo);\n                    }\n                }\n\n                restore();\n            },\n\n            importOptions: function() {\n                var o, options = {}, optionName, value;\n\n                // list of options, surrounded by parens\n                if (! $char('(')) { return null; }\n                do {\n                    o = this.importOption();\n                    if (o) {\n                        optionName = o;\n                        value = true;\n                        switch(optionName) {\n                            case \"css\":\n                                optionName = \"less\";\n                                value = false;\n                            break;\n                            case \"once\":\n                                optionName = \"multiple\";\n                                value = false;\n                            break;\n                        }\n                        options[optionName] = value;\n                        if (! $char(',')) { break; }\n                    }\n                } while (o);\n                expectChar(')');\n                return options;\n            },\n\n            importOption: function() {\n                var opt = $re(/^(less|css|multiple|once|inline|reference)/);\n                if (opt) {\n                    return opt[1];\n                }\n            },\n\n            mediaFeature: function () {\n                var entities = this.entities, nodes = [], e, p;\n                do {\n                    e = entities.keyword() || entities.variable();\n                    if (e) {\n                        nodes.push(e);\n                    } else if ($char('(')) {\n                        p = this.property();\n                        e = this.value();\n                        if ($char(')')) {\n                            if (p && e) {\n                                nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, null, i, env.currentFileInfo, true)));\n                            } else if (e) {\n                                nodes.push(new(tree.Paren)(e));\n                            } else {\n                                return null;\n                            }\n                        } else { return null; }\n                    }\n                } while (e);\n\n                if (nodes.length > 0) {\n                    return new(tree.Expression)(nodes);\n                }\n            },\n\n            mediaFeatures: function () {\n                var entities = this.entities, features = [], e;\n                do {\n                    e = this.mediaFeature();\n                    if (e) {\n                        features.push(e);\n                        if (! $char(',')) { break; }\n                    } else {\n                        e = entities.variable();\n                        if (e) {\n                            features.push(e);\n                            if (! $char(',')) { break; }\n                        }\n                    }\n                } while (e);\n\n                return features.length > 0 ? features : null;\n            },\n\n            media: function () {\n                var features, rules, media, debugInfo;\n\n                if (env.dumpLineNumbers) {\n                    debugInfo = getDebugInfo(i, input, env);\n                }\n\n                if ($re(/^@media/)) {\n                    features = this.mediaFeatures();\n\n                    rules = this.block();\n                    if (rules) {\n                        media = new(tree.Media)(rules, features, i, env.currentFileInfo);\n                        if (env.dumpLineNumbers) {\n                            media.debugInfo = debugInfo;\n                        }\n                        return media;\n                    }\n                }\n            },\n\n            //\n            // A CSS Directive\n            //\n            //     @charset \"utf-8\";\n            //\n            directive: function () {\n                var index = i, name, value, rules, nonVendorSpecificName,\n                    hasIdentifier, hasExpression, hasUnknown, hasBlock = true;\n\n                if (input.charAt(i) !== '@') { return; }\n\n                value = this['import']() || this.media();\n                if (value) {\n                    return value;\n                }\n\n                save();\n\n                name = $re(/^@[a-z-]+/);\n                \n                if (!name) { return; }\n\n                nonVendorSpecificName = name;\n                if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) {\n                    nonVendorSpecificName = \"@\" + name.slice(name.indexOf('-', 2) + 1);\n                }\n\n                switch(nonVendorSpecificName) {\n                    /*\n                    case \"@font-face\":\n                    case \"@viewport\":\n                    case \"@top-left\":\n                    case \"@top-left-corner\":\n                    case \"@top-center\":\n                    case \"@top-right\":\n                    case \"@top-right-corner\":\n                    case \"@bottom-left\":\n                    case \"@bottom-left-corner\":\n                    case \"@bottom-center\":\n                    case \"@bottom-right\":\n                    case \"@bottom-right-corner\":\n                    case \"@left-top\":\n                    case \"@left-middle\":\n                    case \"@left-bottom\":\n                    case \"@right-top\":\n                    case \"@right-middle\":\n                    case \"@right-bottom\":\n                        hasBlock = true;\n                        break;\n                    */\n                    case \"@charset\":\n                        hasIdentifier = true;\n                        hasBlock = false;\n                        break;\n                    case \"@namespace\":\n                        hasExpression = true;\n                        hasBlock = false;\n                        break;\n                    case \"@keyframes\":\n                        hasIdentifier = true;\n                        break;\n                    case \"@host\":\n                    case \"@page\":\n                    case \"@document\":\n                    case \"@supports\":\n                        hasUnknown = true;\n                        break;\n                }\n\n                if (hasIdentifier) {\n                    value = this.entity();\n                    if (!value) {\n                        error(\"expected \" + name + \" identifier\");\n                    }\n                } else if (hasExpression) {\n                    value = this.expression();\n                    if (!value) {\n                        error(\"expected \" + name + \" expression\");\n                    }\n                } else if (hasUnknown) {\n                    value = ($re(/^[^{;]+/) || '').trim();\n                    if (value) {\n                        value = new(tree.Anonymous)(value);\n                    }\n                }\n\n                if (hasBlock) {\n                    rules = this.blockRuleset();\n                }\n\n                if (rules || (!hasBlock && value && $char(';'))) {\n                    forget();\n                    return new(tree.Directive)(name, value, rules, index, env.currentFileInfo, \n                        env.dumpLineNumbers ? getDebugInfo(index, input, env) : null);\n                }\n\n                restore();\n            },\n\n            //\n            // A Value is a comma-delimited list of Expressions\n            //\n            //     font-family: Baskerville, Georgia, serif;\n            //\n            // In a Rule, a Value represents everything after the `:`,\n            // and before the `;`.\n            //\n            value: function () {\n                var e, expressions = [];\n\n                do {\n                    e = this.expression();\n                    if (e) {\n                        expressions.push(e);\n                        if (! $char(',')) { break; }\n                    }\n                } while(e);\n\n                if (expressions.length > 0) {\n                    return new(tree.Value)(expressions);\n                }\n            },\n            important: function () {\n                if (input.charAt(i) === '!') {\n                    return $re(/^! *important/);\n                }\n            },\n            sub: function () {\n                var a, e;\n\n                if ($char('(')) {\n                    a = this.addition();\n                    if (a) {\n                        e = new(tree.Expression)([a]);\n                        expectChar(')');\n                        e.parens = true;\n                        return e;\n                    }\n                }\n            },\n            multiplication: function () {\n                var m, a, op, operation, isSpaced;\n                m = this.operand();\n                if (m) {\n                    isSpaced = isWhitespace(input, i - 1);\n                    while (true) {\n                        if (peek(/^\\/[*\\/]/)) {\n                            break;\n                        }\n                        op = $char('/') || $char('*');\n\n                        if (!op) { break; }\n\n                        a = this.operand();\n\n                        if (!a) { break; }\n\n                        m.parensInOp = true;\n                        a.parensInOp = true;\n                        operation = new(tree.Operation)(op, [operation || m, a], isSpaced);\n                        isSpaced = isWhitespace(input, i - 1);\n                    }\n                    return operation || m;\n                }\n            },\n            addition: function () {\n                var m, a, op, operation, isSpaced;\n                m = this.multiplication();\n                if (m) {\n                    isSpaced = isWhitespace(input, i - 1);\n                    while (true) {\n                        op = $re(/^[-+]\\s+/) || (!isSpaced && ($char('+') || $char('-')));\n                        if (!op) {\n                            break;\n                        }\n                        a = this.multiplication();\n                        if (!a) {\n                            break;\n                        }\n                        \n                        m.parensInOp = true;\n                        a.parensInOp = true;\n                        operation = new(tree.Operation)(op, [operation || m, a], isSpaced);\n                        isSpaced = isWhitespace(input, i - 1);\n                    }\n                    return operation || m;\n                }\n            },\n            conditions: function () {\n                var a, b, index = i, condition;\n\n                a = this.condition();\n                if (a) {\n                    while (true) {\n                        if (!peek(/^,\\s*(not\\s*)?\\(/) || !$char(',')) {\n                            break;\n                        }\n                        b = this.condition();\n                        if (!b) {\n                            break;\n                        }\n                        condition = new(tree.Condition)('or', condition || a, b, index);\n                    }\n                    return condition || a;\n                }\n            },\n            condition: function () {\n                var entities = this.entities, index = i, negate = false,\n                    a, b, c, op;\n\n                if ($re(/^not/)) { negate = true; }\n                expectChar('(');\n                a = this.addition() || entities.keyword() || entities.quoted();\n                if (a) {\n                    op = $re(/^(?:>=|<=|=<|[<=>])/);\n                    if (op) {\n                        b = this.addition() || entities.keyword() || entities.quoted();\n                        if (b) {\n                            c = new(tree.Condition)(op, a, b, index, negate);\n                        } else {\n                            error('expected expression');\n                        }\n                    } else {\n                        c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate);\n                    }\n                    expectChar(')');\n                    return $re(/^and/) ? new(tree.Condition)('and', c, this.condition()) : c;\n                }\n            },\n\n            //\n            // An operand is anything that can be part of an operation,\n            // such as a Color, or a Variable\n            //\n            operand: function () {\n                var entities = this.entities,\n                    p = input.charAt(i + 1), negate;\n\n                if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $char('-'); }\n                var o = this.sub() || entities.dimension() ||\n                        entities.color() || entities.variable() ||\n                        entities.call();\n\n                if (negate) {\n                    o.parensInOp = true;\n                    o = new(tree.Negative)(o);\n                }\n\n                return o;\n            },\n\n            //\n            // Expressions either represent mathematical operations,\n            // or white-space delimited Entities.\n            //\n            //     1px solid black\n            //     @var * 2\n            //\n            expression: function () {\n                var entities = [], e, delim;\n\n                do {\n                    e = this.addition() || this.entity();\n                    if (e) {\n                        entities.push(e);\n                        // operations do not allow keyword \"/\" dimension (e.g. small/20px) so we support that here\n                        if (!peek(/^\\/[\\/*]/)) {\n                            delim = $char('/');\n                            if (delim) {\n                                entities.push(new(tree.Anonymous)(delim));\n                            }\n                        }\n                    }\n                } while (e);\n                if (entities.length > 0) {\n                    return new(tree.Expression)(entities);\n                }\n            },\n            property: function () {\n                var name = $re(/^(\\*?-?[_a-zA-Z0-9-]+)\\s*:/);\n                if (name) {\n                    return name[1];\n                }\n            },\n            ruleProperty: function () {\n                var c = current, name = [], index = [], length = 0, s, k;\n                \n                function match(re) {\n                    var a = re.exec(c);\n                    if (a) {\n                        index.push(i + length);\n                        length += a[0].length;\n                        c = c.slice(a[1].length);\n                        return name.push(a[1]);\n                    }\n                }\n\n                match(/^(\\*?)/);\n                while (match(/^((?:[\\w-]+)|(?:@\\{[\\w-]+\\}))/)); // !\n                if ((name.length > 1) && match(/^\\s*((?:\\+_|\\+)?)\\s*:/)) {\n                    // at last, we have the complete match now. move forward, \n                    // convert name particles to tree objects and return:\n                    skipWhitespace(length);\n                    if (name[0] === '') {\n                        name.shift();\n                        index.shift();\n                    }\n                    for (k = 0; k < name.length; k++) {\n                        s = name[k];\n                        name[k] = (s.charAt(0) !== '@')\n                            ? new(tree.Keyword)(s)\n                            : new(tree.Variable)('@' + s.slice(2, -1), \n                                index[k], env.currentFileInfo);\n                    }\n                    return name;\n                }\n            }\n        }\n    };\n    return parser;\n};\nless.Parser.serializeVars = function(vars) {\n    var s = '';\n\n    for (var name in vars) {\n        if (Object.hasOwnProperty.call(vars, name)) {\n            var value = vars[name];\n            s += ((name[0] === '@') ? '' : '@') + name +': '+ value +\n                    ((('' + value).slice(-1) === ';') ? '' : ';');\n        }\n    }\n\n    return s;\n};\n\n(function (tree) {\n\ntree.functions = {\n    rgb: function (r, g, b) {\n        return this.rgba(r, g, b, 1.0);\n    },\n    rgba: function (r, g, b, a) {\n        var rgb = [r, g, b].map(function (c) { return scaled(c, 255); });\n        a = number(a);\n        return new(tree.Color)(rgb, a);\n    },\n    hsl: function (h, s, l) {\n        return this.hsla(h, s, l, 1.0);\n    },\n    hsla: function (h, s, l, a) {\n        function hue(h) {\n            h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);\n            if      (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; }\n            else if (h * 2 < 1) { return m2; }\n            else if (h * 3 < 2) { return m1 + (m2 - m1) * (2/3 - h) * 6; }\n            else                { return m1; }\n        }\n\n        h = (number(h) % 360) / 360;\n        s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a));\n\n        var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;\n        var m1 = l * 2 - m2;\n\n        return this.rgba(hue(h + 1/3) * 255,\n                         hue(h)       * 255,\n                         hue(h - 1/3) * 255,\n                         a);\n    },\n\n    hsv: function(h, s, v) {\n        return this.hsva(h, s, v, 1.0);\n    },\n\n    hsva: function(h, s, v, a) {\n        h = ((number(h) % 360) / 360) * 360;\n        s = number(s); v = number(v); a = number(a);\n\n        var i, f;\n        i = Math.floor((h / 60) % 6);\n        f = (h / 60) - i;\n\n        var vs = [v,\n                  v * (1 - s),\n                  v * (1 - f * s),\n                  v * (1 - (1 - f) * s)];\n        var perm = [[0, 3, 1],\n                    [2, 0, 1],\n                    [1, 0, 3],\n                    [1, 2, 0],\n                    [3, 1, 0],\n                    [0, 1, 2]];\n\n        return this.rgba(vs[perm[i][0]] * 255,\n                         vs[perm[i][1]] * 255,\n                         vs[perm[i][2]] * 255,\n                         a);\n    },\n\n    hue: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().h));\n    },\n    saturation: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%');\n    },\n    lightness: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%');\n    },\n    hsvhue: function(color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().h));\n    },\n    hsvsaturation: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().s * 100), '%');\n    },\n    hsvvalue: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().v * 100), '%');\n    },\n    red: function (color) {\n        return new(tree.Dimension)(color.rgb[0]);\n    },\n    green: function (color) {\n        return new(tree.Dimension)(color.rgb[1]);\n    },\n    blue: function (color) {\n        return new(tree.Dimension)(color.rgb[2]);\n    },\n    alpha: function (color) {\n        return new(tree.Dimension)(color.toHSL().a);\n    },\n    luma: function (color) {\n        return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%');\n    },\n    luminance: function (color) {\n        var luminance =\n            (0.2126 * color.rgb[0] / 255)\n          + (0.7152 * color.rgb[1] / 255)\n          + (0.0722 * color.rgb[2] / 255);\n\n        return new(tree.Dimension)(Math.round(luminance * color.alpha * 100), '%');\n    },\n    saturate: function (color, amount) {\n        // filter: saturate(3.2);\n        // should be kept as is, so check for color\n        if (!color.rgb) {\n            return null;\n        }\n        var hsl = color.toHSL();\n\n        hsl.s += amount.value / 100;\n        hsl.s = clamp(hsl.s);\n        return hsla(hsl);\n    },\n    desaturate: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.s -= amount.value / 100;\n        hsl.s = clamp(hsl.s);\n        return hsla(hsl);\n    },\n    lighten: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.l += amount.value / 100;\n        hsl.l = clamp(hsl.l);\n        return hsla(hsl);\n    },\n    darken: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.l -= amount.value / 100;\n        hsl.l = clamp(hsl.l);\n        return hsla(hsl);\n    },\n    fadein: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a += amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    fadeout: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a -= amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    fade: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a = amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    spin: function (color, amount) {\n        var hsl = color.toHSL();\n        var hue = (hsl.h + amount.value) % 360;\n\n        hsl.h = hue < 0 ? 360 + hue : hue;\n\n        return hsla(hsl);\n    },\n    //\n    // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein\n    // http://sass-lang.com\n    //\n    mix: function (color1, color2, weight) {\n        if (!weight) {\n            weight = new(tree.Dimension)(50);\n        }\n        var p = weight.value / 100.0;\n        var w = p * 2 - 1;\n        var a = color1.toHSL().a - color2.toHSL().a;\n\n        var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;\n        var w2 = 1 - w1;\n\n        var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2,\n                   color1.rgb[1] * w1 + color2.rgb[1] * w2,\n                   color1.rgb[2] * w1 + color2.rgb[2] * w2];\n\n        var alpha = color1.alpha * p + color2.alpha * (1 - p);\n\n        return new(tree.Color)(rgb, alpha);\n    },\n    greyscale: function (color) {\n        return this.desaturate(color, new(tree.Dimension)(100));\n    },\n    contrast: function (color, dark, light, threshold) {\n        // filter: contrast(3.2);\n        // should be kept as is, so check for color\n        if (!color.rgb) {\n            return null;\n        }\n        if (typeof light === 'undefined') {\n            light = this.rgba(255, 255, 255, 1.0);\n        }\n        if (typeof dark === 'undefined') {\n            dark = this.rgba(0, 0, 0, 1.0);\n        }\n        //Figure out which is actually light and dark!\n        if (dark.luma() > light.luma()) {\n            var t = light;\n            light = dark;\n            dark = t;\n        }\n        if (typeof threshold === 'undefined') {\n            threshold = 0.43;\n        } else {\n            threshold = number(threshold);\n        }\n        if (color.luma() < threshold) {\n            return light;\n        } else {\n            return dark;\n        }\n    },\n    e: function (str) {\n        return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str);\n    },\n    escape: function (str) {\n        return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, \"%3D\").replace(/:/g, \"%3A\").replace(/#/g, \"%23\").replace(/;/g, \"%3B\").replace(/\\(/g, \"%28\").replace(/\\)/g, \"%29\"));\n    },\n    replace: function (string, pattern, replacement, flags) {\n        var result = string.value;\n\n        result = result.replace(new RegExp(pattern.value, flags ? flags.value : ''), replacement.value);\n        return new(tree.Quoted)(string.quote || '', result, string.escaped);\n    },\n    '%': function (string /* arg, arg, ...*/) {\n        var args = Array.prototype.slice.call(arguments, 1),\n            result = string.value;\n\n        for (var i = 0; i < args.length; i++) {\n            /*jshint loopfunc:true */\n            result = result.replace(/%[sda]/i, function(token) {\n                var value = token.match(/s/i) ? args[i].value : args[i].toCSS();\n                return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value;\n            });\n        }\n        result = result.replace(/%%/g, '%');\n        return new(tree.Quoted)(string.quote || '', result, string.escaped);\n    },\n    unit: function (val, unit) {\n        if(!(val instanceof tree.Dimension)) {\n            throw { type: \"Argument\", message: \"the first argument to unit must be a number\" + (val instanceof tree.Operation ? \". Have you forgotten parenthesis?\" : \"\") };\n        }\n        if (unit) {\n            if (unit instanceof tree.Keyword) {\n                unit = unit.value;\n            } else {\n                unit = unit.toCSS();\n            }\n        } else {\n            unit = \"\";\n        }\n        return new(tree.Dimension)(val.value, unit);\n    },\n    convert: function (val, unit) {\n        return val.convertTo(unit.value);\n    },\n    round: function (n, f) {\n        var fraction = typeof(f) === \"undefined\" ? 0 : f.value;\n        return _math(function(num) { return num.toFixed(fraction); }, null, n);\n    },\n    pi: function () {\n        return new(tree.Dimension)(Math.PI);\n    },\n    mod: function(a, b) {\n        return new(tree.Dimension)(a.value % b.value, a.unit);\n    },\n    pow: function(x, y) {\n        if (typeof x === \"number\" && typeof y === \"number\") {\n            x = new(tree.Dimension)(x);\n            y = new(tree.Dimension)(y);\n        } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) {\n            throw { type: \"Argument\", message: \"arguments must be numbers\" };\n        }\n\n        return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit);\n    },\n    _minmax: function (isMin, args) {\n        args = Array.prototype.slice.call(args);\n        switch(args.length) {\n            case 0: throw { type: \"Argument\", message: \"one or more arguments required\" };\n        }\n        var i, j, current, currentUnified, referenceUnified, unit, unitStatic, unitClone,\n            order  = [], // elems only contains original argument values.\n            values = {}; // key is the unit.toString() for unified tree.Dimension values,\n                         // value is the index into the order array.\n        for (i = 0; i < args.length; i++) {\n            current = args[i];\n            if (!(current instanceof tree.Dimension)) {\n                if(Array.isArray(args[i].value)) {\n                    Array.prototype.push.apply(args, Array.prototype.slice.call(args[i].value));\n                }\n                continue;\n            }\n            currentUnified = current.unit.toString() === \"\" && unitClone !== undefined ? new(tree.Dimension)(current.value, unitClone).unify() : current.unify();\n            unit = currentUnified.unit.toString() === \"\" && unitStatic !== undefined ? unitStatic : currentUnified.unit.toString();\t\t\t\n            unitStatic = unit !== \"\" && unitStatic === undefined || unit !== \"\" && order[0].unify().unit.toString() === \"\" ? unit : unitStatic;\n            unitClone = unit !== \"\" && unitClone === undefined ? current.unit.toString() : unitClone;\n            j = values[\"\"] !== undefined && unit !== \"\" && unit === unitStatic ? values[\"\"] : values[unit];\n            if (j === undefined) {\n                if(unitStatic !== undefined && unit !== unitStatic) {\n                    throw{ type: \"Argument\", message: \"incompatible types\" };\n                }\n                values[unit] = order.length;\n                order.push(current);\n                continue;\n            }\n            referenceUnified = order[j].unit.toString() === \"\" && unitClone !== undefined ? new(tree.Dimension)(order[j].value, unitClone).unify() : order[j].unify();\n            if ( isMin && currentUnified.value < referenceUnified.value ||\n                !isMin && currentUnified.value > referenceUnified.value) {\n                order[j] = current;\n            }\n        }\n        if (order.length == 1) {\n            return order[0];\n        }\n        args = order.map(function (a) { return a.toCSS(this.env); }).join(this.env.compress ? \",\" : \", \");\n        return new(tree.Anonymous)((isMin ? \"min\" : \"max\") + \"(\" + args + \")\");\n    },\n    min: function () {\n        return this._minmax(true, arguments);\n    },\n    max: function () {\n        return this._minmax(false, arguments);\n    },\n    \"get-unit\": function (n) {\n        return new(tree.Anonymous)(n.unit);\n    },\n    argb: function (color) {\n        return new(tree.Anonymous)(color.toARGB());\n    },\n    percentage: function (n) {\n        return new(tree.Dimension)(n.value * 100, '%');\n    },\n    color: function (n) {\n        if (n instanceof tree.Quoted) {\n            var colorCandidate = n.value,\n                returnColor;\n            returnColor = tree.Color.fromKeyword(colorCandidate);\n            if (returnColor) {\n                return returnColor;\n            }\n            if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/.test(colorCandidate)) {\n                return new(tree.Color)(colorCandidate.slice(1));\n            }\n            throw { type: \"Argument\", message: \"argument must be a color keyword or 3/6 digit hex e.g. #FFF\" };\n        } else {\n            throw { type: \"Argument\", message: \"argument must be a string\" };\n        }\n    },\n    iscolor: function (n) {\n        return this._isa(n, tree.Color);\n    },\n    isnumber: function (n) {\n        return this._isa(n, tree.Dimension);\n    },\n    isstring: function (n) {\n        return this._isa(n, tree.Quoted);\n    },\n    iskeyword: function (n) {\n        return this._isa(n, tree.Keyword);\n    },\n    isurl: function (n) {\n        return this._isa(n, tree.URL);\n    },\n    ispixel: function (n) {\n        return this.isunit(n, 'px');\n    },\n    ispercentage: function (n) {\n        return this.isunit(n, '%');\n    },\n    isem: function (n) {\n        return this.isunit(n, 'em');\n    },\n    isunit: function (n, unit) {\n        return (n instanceof tree.Dimension) && n.unit.is(unit.value || unit) ? tree.True : tree.False;\n    },\n    _isa: function (n, Type) {\n        return (n instanceof Type) ? tree.True : tree.False;\n    },\n    tint: function(color, amount) {\n        return this.mix(this.rgb(255,255,255), color, amount);\n    },\n    shade: function(color, amount) {\n        return this.mix(this.rgb(0, 0, 0), color, amount);\n    },   \n    extract: function(values, index) {\n        index = index.value - 1; // (1-based index)       \n        // handle non-array values as an array of length 1\n        // return 'undefined' if index is invalid\n        return Array.isArray(values.value) \n            ? values.value[index] : Array(values)[index];\n    },\n    length: function(values) {\n        var n = Array.isArray(values.value) ? values.value.length : 1;\n        return new tree.Dimension(n);\n    },\n\n    \"data-uri\": function(mimetypeNode, filePathNode) {\n\n        if (typeof window !== 'undefined') {\n            return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env);\n        }\n\n        var mimetype = mimetypeNode.value;\n        var filePath = (filePathNode && filePathNode.value);\n\n        var fs = require('fs'),\n            path = require('path'),\n            useBase64 = false;\n\n        if (arguments.length < 2) {\n            filePath = mimetype;\n        }\n\n        if (this.env.isPathRelative(filePath)) {\n            if (this.currentFileInfo.relativeUrls) {\n                filePath = path.join(this.currentFileInfo.currentDirectory, filePath);\n            } else {\n                filePath = path.join(this.currentFileInfo.entryPath, filePath);\n            }\n        }\n\n        // detect the mimetype if not given\n        if (arguments.length < 2) {\n            var mime;\n            try {\n                mime = require('mime');\n            } catch (ex) {\n                mime = tree._mime;\n            }\n\n            mimetype = mime.lookup(filePath);\n\n            // use base 64 unless it's an ASCII or UTF-8 format\n            var charset = mime.charsets.lookup(mimetype);\n            useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0;\n            if (useBase64) { mimetype += ';base64'; }\n        }\n        else {\n            useBase64 = /;base64$/.test(mimetype);\n        }\n\n        var buf = fs.readFileSync(filePath);\n\n        // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded\n        // and the --ieCompat flag is enabled, return a normal url() instead.\n        var DATA_URI_MAX_KB = 32,\n            fileSizeInKB = parseInt((buf.length / 1024), 10);\n        if (fileSizeInKB >= DATA_URI_MAX_KB) {\n\n            if (this.env.ieCompat !== false) {\n                if (!this.env.silent) {\n                    console.warn(\"Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!\", filePath, fileSizeInKB, DATA_URI_MAX_KB);\n                }\n\n                return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env);\n            }\n        }\n\n        buf = useBase64 ? buf.toString('base64')\n                        : encodeURIComponent(buf);\n\n        var uri = \"\\\"data:\" + mimetype + ',' + buf + \"\\\"\";\n        return new(tree.URL)(new(tree.Anonymous)(uri));\n    },\n\n    \"svg-gradient\": function(direction) {\n\n        function throwArgumentDescriptor() {\n            throw { type: \"Argument\", message: \"svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]\" };\n        }\n\n        if (arguments.length < 3) {\n            throwArgumentDescriptor();\n        }\n        var stops = Array.prototype.slice.call(arguments, 1),\n            gradientDirectionSvg,\n            gradientType = \"linear\",\n            rectangleDimension = 'x=\"0\" y=\"0\" width=\"1\" height=\"1\"',\n            useBase64 = true,\n            renderEnv = {compress: false},\n            returner,\n            directionValue = direction.toCSS(renderEnv),\n            i, color, position, positionValue, alpha;\n\n        switch (directionValue) {\n            case \"to bottom\":\n                gradientDirectionSvg = 'x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\"';\n                break;\n            case \"to right\":\n                gradientDirectionSvg = 'x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\"';\n                break;\n            case \"to bottom right\":\n                gradientDirectionSvg = 'x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"';\n                break;\n            case \"to top right\":\n                gradientDirectionSvg = 'x1=\"0%\" y1=\"100%\" x2=\"100%\" y2=\"0%\"';\n                break;\n            case \"ellipse\":\n            case \"ellipse at center\":\n                gradientType = \"radial\";\n                gradientDirectionSvg = 'cx=\"50%\" cy=\"50%\" r=\"75%\"';\n                rectangleDimension = 'x=\"-50\" y=\"-50\" width=\"101\" height=\"101\"';\n                break;\n            default:\n                throw { type: \"Argument\", message: \"svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'\" };\n        }\n        returner = '<?xml version=\"1.0\" ?>' +\n            '<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"100%\" height=\"100%\" viewBox=\"0 0 1 1\" preserveAspectRatio=\"none\">' +\n            '<' + gradientType + 'Gradient id=\"gradient\" gradientUnits=\"userSpaceOnUse\" ' + gradientDirectionSvg + '>';\n\n        for (i = 0; i < stops.length; i+= 1) {\n            if (stops[i].value) {\n                color = stops[i].value[0];\n                position = stops[i].value[1];\n            } else {\n                color = stops[i];\n                position = undefined;\n            }\n\n            if (!(color instanceof tree.Color) || (!((i === 0 || i+1 === stops.length) && position === undefined) && !(position instanceof tree.Dimension))) {\n                throwArgumentDescriptor();\n            }\n            positionValue = position ? position.toCSS(renderEnv) : i === 0 ? \"0%\" : \"100%\";\n            alpha = color.alpha;\n            returner += '<stop offset=\"' + positionValue + '\" stop-color=\"' + color.toRGB() + '\"' + (alpha < 1 ? ' stop-opacity=\"' + alpha + '\"' : '') + '/>';\n        }\n        returner += '</' + gradientType + 'Gradient>' +\n                    '<rect ' + rectangleDimension + ' fill=\"url(#gradient)\" /></svg>';\n\n        if (useBase64) {\n            try {\n                returner = require('./encoder').encodeBase64(returner); // TODO browser implementation\n            } catch(e) {\n                useBase64 = false;\n            }\n        }\n\n        returner = \"'data:image/svg+xml\" + (useBase64 ? \";base64\" : \"\") + \",\" + returner + \"'\";\n        return new(tree.URL)(new(tree.Anonymous)(returner));\n    }\n};\n\n// these static methods are used as a fallback when the optional 'mime' dependency is missing\ntree._mime = {\n    // this map is intentionally incomplete\n    // if you want more, install 'mime' dep\n    _types: {\n        '.htm' : 'text/html',\n        '.html': 'text/html',\n        '.gif' : 'image/gif',\n        '.jpg' : 'image/jpeg',\n        '.jpeg': 'image/jpeg',\n        '.png' : 'image/png'\n    },\n    lookup: function (filepath) {\n        var ext = require('path').extname(filepath),\n            type = tree._mime._types[ext];\n        if (type === undefined) {\n            throw new Error('Optional dependency \"mime\" is required for ' + ext);\n        }\n        return type;\n    },\n    charsets: {\n        lookup: function (type) {\n            // assumes all text types are UTF-8\n            return type && (/^text\\//).test(type) ? 'UTF-8' : '';\n        }\n    }\n};\n\n// Math\n\nvar mathFunctions = {\n // name,  unit\n    ceil:  null, \n    floor: null, \n    sqrt:  null, \n    abs:   null,\n    tan:   \"\", \n    sin:   \"\", \n    cos:   \"\",\n    atan:  \"rad\", \n    asin:  \"rad\", \n    acos:  \"rad\"\n};\n\nfunction _math(fn, unit, n) {\n    if (!(n instanceof tree.Dimension)) {\n        throw { type: \"Argument\", message: \"argument must be a number\" };\n    }\n    if (unit == null) {\n        unit = n.unit;\n    } else {\n        n = n.unify();\n    }\n    return new(tree.Dimension)(fn(parseFloat(n.value)), unit);\n}\n\n// ~ End of Math\n\n// Color Blending\n// ref: http://www.w3.org/TR/compositing-1\n\nfunction colorBlend(mode, color1, color2) {\n    var ab = color1.alpha, cb, // backdrop\n        as = color2.alpha, cs, // source\n        ar, cr, r = [];        // result\n        \n    ar = as + ab * (1 - as);\n    for (var i = 0; i < 3; i++) {\n        cb = color1.rgb[i] / 255;\n        cs = color2.rgb[i] / 255;\n        cr = mode(cb, cs);\n        if (ar) {\n            cr = (as * cs + ab * (cb \n                - as * (cb + cs - cr))) / ar;\n        }\n        r[i] = cr * 255;\n    }\n    \n    return new(tree.Color)(r, ar);\n}\n\nvar colorBlendMode = {\n    multiply: function(cb, cs) {\n        return cb * cs;\n    },\n    screen: function(cb, cs) {\n        return cb + cs - cb * cs;\n    },   \n    overlay: function(cb, cs) {\n        cb *= 2;\n        return (cb <= 1)\n            ? colorBlendMode.multiply(cb, cs)\n            : colorBlendMode.screen(cb - 1, cs);\n    },\n    softlight: function(cb, cs) {\n        var d = 1, e = cb;\n        if (cs > 0.5) {\n            e = 1;\n            d = (cb > 0.25) ? Math.sqrt(cb)\n                : ((16 * cb - 12) * cb + 4) * cb;\n        }            \n        return cb - (1 - 2 * cs) * e * (d - cb);\n    },\n    hardlight: function(cb, cs) {\n        return colorBlendMode.overlay(cs, cb);\n    },\n    difference: function(cb, cs) {\n        return Math.abs(cb - cs);\n    },\n    exclusion: function(cb, cs) {\n        return cb + cs - 2 * cb * cs;\n    },\n\n    // non-w3c functions:\n    average: function(cb, cs) {\n        return (cb + cs) / 2;\n    },\n    negation: function(cb, cs) {\n        return 1 - Math.abs(cb + cs - 1);\n    }\n};\n\n// ~ End of Color Blending\n\ntree.defaultFunc = {\n    eval: function () {\n        var v = this.value_, e = this.error_;\n        if (e) {\n            throw e;\n        }\n        if (v != null) {\n            return v ? tree.True : tree.False;\n        }\n    },\n    value: function (v) {\n        this.value_ = v;\n    },\n    error: function (e) {\n        this.error_ = e;\n    },\n    reset: function () {\n        this.value_ = this.error_ = null;\n    }\n};\n\nfunction initFunctions() {\n    var f, tf = tree.functions;\n    \n    // math\n    for (f in mathFunctions) {\n        if (mathFunctions.hasOwnProperty(f)) {\n            tf[f] = _math.bind(null, Math[f], mathFunctions[f]);\n        }\n    }\n    \n    // color blending\n    for (f in colorBlendMode) {\n        if (colorBlendMode.hasOwnProperty(f)) {\n            tf[f] = colorBlend.bind(null, colorBlendMode[f]);\n        }\n    }\n    \n    // default\n    f = tree.defaultFunc;\n    tf[\"default\"] = f.eval.bind(f);\n    \n} initFunctions();\n\nfunction hsla(color) {\n    return tree.functions.hsla(color.h, color.s, color.l, color.a);\n}\n\nfunction scaled(n, size) {\n    if (n instanceof tree.Dimension && n.unit.is('%')) {\n        return parseFloat(n.value * size / 100);\n    } else {\n        return number(n);\n    }\n}\n\nfunction number(n) {\n    if (n instanceof tree.Dimension) {\n        return parseFloat(n.unit.is('%') ? n.value / 100 : n.value);\n    } else if (typeof(n) === 'number') {\n        return n;\n    } else {\n        throw {\n            error: \"RuntimeError\",\n            message: \"color functions take numbers as parameters\"\n        };\n    }\n}\n\nfunction clamp(val) {\n    return Math.min(1, Math.max(0, val));\n}\n\ntree.fround = function(env, value) {\n    var p;\n    if (env && (env.numPrecision != null)) {\n        p = Math.pow(10, env.numPrecision);\n        return Math.round(value * p) / p;\n    } else {\n        return value;\n    }\n};\n\ntree.functionCall = function(env, currentFileInfo) {\n    this.env = env;\n    this.currentFileInfo = currentFileInfo;\n};\n\ntree.functionCall.prototype = tree.functions;\n\n})(require('./tree'));\n\n(function (tree) {\n    tree.colors = {\n        'aliceblue':'#f0f8ff',\n        'antiquewhite':'#faebd7',\n        'aqua':'#00ffff',\n        'aquamarine':'#7fffd4',\n        'azure':'#f0ffff',\n        'beige':'#f5f5dc',\n        'bisque':'#ffe4c4',\n        'black':'#000000',\n        'blanchedalmond':'#ffebcd',\n        'blue':'#0000ff',\n        'blueviolet':'#8a2be2',\n        'brown':'#a52a2a',\n        'burlywood':'#deb887',\n        'cadetblue':'#5f9ea0',\n        'chartreuse':'#7fff00',\n        'chocolate':'#d2691e',\n        'coral':'#ff7f50',\n        'cornflowerblue':'#6495ed',\n        'cornsilk':'#fff8dc',\n        'crimson':'#dc143c',\n        'cyan':'#00ffff',\n        'darkblue':'#00008b',\n        'darkcyan':'#008b8b',\n        'darkgoldenrod':'#b8860b',\n        'darkgray':'#a9a9a9',\n        'darkgrey':'#a9a9a9',\n        'darkgreen':'#006400',\n        'darkkhaki':'#bdb76b',\n        'darkmagenta':'#8b008b',\n        'darkolivegreen':'#556b2f',\n        'darkorange':'#ff8c00',\n        'darkorchid':'#9932cc',\n        'darkred':'#8b0000',\n        'darksalmon':'#e9967a',\n        'darkseagreen':'#8fbc8f',\n        'darkslateblue':'#483d8b',\n        'darkslategray':'#2f4f4f',\n        'darkslategrey':'#2f4f4f',\n        'darkturquoise':'#00ced1',\n        'darkviolet':'#9400d3',\n        'deeppink':'#ff1493',\n        'deepskyblue':'#00bfff',\n        'dimgray':'#696969',\n        'dimgrey':'#696969',\n        'dodgerblue':'#1e90ff',\n        'firebrick':'#b22222',\n        'floralwhite':'#fffaf0',\n        'forestgreen':'#228b22',\n        'fuchsia':'#ff00ff',\n        'gainsboro':'#dcdcdc',\n        'ghostwhite':'#f8f8ff',\n        'gold':'#ffd700',\n        'goldenrod':'#daa520',\n        'gray':'#808080',\n        'grey':'#808080',\n        'green':'#008000',\n        'greenyellow':'#adff2f',\n        'honeydew':'#f0fff0',\n        'hotpink':'#ff69b4',\n        'indianred':'#cd5c5c',\n        'indigo':'#4b0082',\n        'ivory':'#fffff0',\n        'khaki':'#f0e68c',\n        'lavender':'#e6e6fa',\n        'lavenderblush':'#fff0f5',\n        'lawngreen':'#7cfc00',\n        'lemonchiffon':'#fffacd',\n        'lightblue':'#add8e6',\n        'lightcoral':'#f08080',\n        'lightcyan':'#e0ffff',\n        'lightgoldenrodyellow':'#fafad2',\n        'lightgray':'#d3d3d3',\n        'lightgrey':'#d3d3d3',\n        'lightgreen':'#90ee90',\n        'lightpink':'#ffb6c1',\n        'lightsalmon':'#ffa07a',\n        'lightseagreen':'#20b2aa',\n        'lightskyblue':'#87cefa',\n        'lightslategray':'#778899',\n        'lightslategrey':'#778899',\n        'lightsteelblue':'#b0c4de',\n        'lightyellow':'#ffffe0',\n        'lime':'#00ff00',\n        'limegreen':'#32cd32',\n        'linen':'#faf0e6',\n        'magenta':'#ff00ff',\n        'maroon':'#800000',\n        'mediumaquamarine':'#66cdaa',\n        'mediumblue':'#0000cd',\n        'mediumorchid':'#ba55d3',\n        'mediumpurple':'#9370d8',\n        'mediumseagreen':'#3cb371',\n        'mediumslateblue':'#7b68ee',\n        'mediumspringgreen':'#00fa9a',\n        'mediumturquoise':'#48d1cc',\n        'mediumvioletred':'#c71585',\n        'midnightblue':'#191970',\n        'mintcream':'#f5fffa',\n        'mistyrose':'#ffe4e1',\n        'moccasin':'#ffe4b5',\n        'navajowhite':'#ffdead',\n        'navy':'#000080',\n        'oldlace':'#fdf5e6',\n        'olive':'#808000',\n        'olivedrab':'#6b8e23',\n        'orange':'#ffa500',\n        'orangered':'#ff4500',\n        'orchid':'#da70d6',\n        'palegoldenrod':'#eee8aa',\n        'palegreen':'#98fb98',\n        'paleturquoise':'#afeeee',\n        'palevioletred':'#d87093',\n        'papayawhip':'#ffefd5',\n        'peachpuff':'#ffdab9',\n        'peru':'#cd853f',\n        'pink':'#ffc0cb',\n        'plum':'#dda0dd',\n        'powderblue':'#b0e0e6',\n        'purple':'#800080',\n        'red':'#ff0000',\n        'rosybrown':'#bc8f8f',\n        'royalblue':'#4169e1',\n        'saddlebrown':'#8b4513',\n        'salmon':'#fa8072',\n        'sandybrown':'#f4a460',\n        'seagreen':'#2e8b57',\n        'seashell':'#fff5ee',\n        'sienna':'#a0522d',\n        'silver':'#c0c0c0',\n        'skyblue':'#87ceeb',\n        'slateblue':'#6a5acd',\n        'slategray':'#708090',\n        'slategrey':'#708090',\n        'snow':'#fffafa',\n        'springgreen':'#00ff7f',\n        'steelblue':'#4682b4',\n        'tan':'#d2b48c',\n        'teal':'#008080',\n        'thistle':'#d8bfd8',\n        'tomato':'#ff6347',\n        'turquoise':'#40e0d0',\n        'violet':'#ee82ee',\n        'wheat':'#f5deb3',\n        'white':'#ffffff',\n        'whitesmoke':'#f5f5f5',\n        'yellow':'#ffff00',\n        'yellowgreen':'#9acd32'\n    };\n})(require('./tree'));\n\n(function (tree) {\n\ntree.debugInfo = function(env, ctx, lineSeperator) {\n    var result=\"\";\n    if (env.dumpLineNumbers && !env.compress) {\n        switch(env.dumpLineNumbers) {\n            case 'comments':\n                result = tree.debugInfo.asComment(ctx);\n                break;\n            case 'mediaquery':\n                result = tree.debugInfo.asMediaQuery(ctx);\n                break;\n            case 'all':\n                result = tree.debugInfo.asComment(ctx) + (lineSeperator || \"\") + tree.debugInfo.asMediaQuery(ctx);\n                break;\n        }\n    }\n    return result;\n};\n\ntree.debugInfo.asComment = function(ctx) {\n    return '/* line ' + ctx.debugInfo.lineNumber + ', ' + ctx.debugInfo.fileName + ' */\\n';\n};\n\ntree.debugInfo.asMediaQuery = function(ctx) {\n    return '@media -sass-debug-info{filename{font-family:' +\n        ('file://' + ctx.debugInfo.fileName).replace(/([.:\\/\\\\])/g, function (a) {\n            if (a == '\\\\') {\n                a = '\\/';\n            }\n            return '\\\\' + a;\n        }) +\n        '}line{font-family:\\\\00003' + ctx.debugInfo.lineNumber + '}}\\n';\n};\n\ntree.find = function (obj, fun) {\n    for (var i = 0, r; i < obj.length; i++) {\n        r = fun.call(obj, obj[i]);\n        if (r) { return r; }\n    }\n    return null;\n};\n\ntree.jsify = function (obj) {\n    if (Array.isArray(obj.value) && (obj.value.length > 1)) {\n        return '[' + obj.value.map(function (v) { return v.toCSS(false); }).join(', ') + ']';\n    } else {\n        return obj.toCSS(false);\n    }\n};\n\ntree.toCSS = function (env) {\n    var strs = [];\n    this.genCSS(env, {\n        add: function(chunk, fileInfo, index) {\n            strs.push(chunk);\n        },\n        isEmpty: function () {\n            return strs.length === 0;\n        }\n    });\n    return strs.join('');\n};\n\ntree.outputRuleset = function (env, output, rules) {\n    var ruleCnt = rules.length, i;\n    env.tabLevel = (env.tabLevel | 0) + 1;\n\n    // Compressed\n    if (env.compress) {\n        output.add('{');\n        for (i = 0; i < ruleCnt; i++) {\n            rules[i].genCSS(env, output);\n        }\n        output.add('}');\n        env.tabLevel--;\n        return;\n    }\n\n    // Non-compressed\n    var tabSetStr = '\\n' + Array(env.tabLevel).join(\"  \"), tabRuleStr = tabSetStr + \"  \";\n    if (!ruleCnt) {\n        output.add(\" {\" + tabSetStr + '}');\n    } else {\n        output.add(\" {\" + tabRuleStr);\n        rules[0].genCSS(env, output);\n        for (i = 1; i < ruleCnt; i++) {\n            output.add(tabRuleStr);\n            rules[i].genCSS(env, output);\n        }\n        output.add(tabSetStr + '}');\n    }\n\n    env.tabLevel--;\n};\n\n})(require('./tree'));\n\n(function (tree) {\n\ntree.Alpha = function (val) {\n    this.value = val;\n};\ntree.Alpha.prototype = {\n    type: \"Alpha\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        if (this.value.eval) { return new tree.Alpha(this.value.eval(env)); }\n        return this;\n    },\n    genCSS: function (env, output) {\n        output.add(\"alpha(opacity=\");\n\n        if (this.value.genCSS) {\n            this.value.genCSS(env, output);\n        } else {\n            output.add(this.value);\n        }\n\n        output.add(\")\");\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Anonymous = function (string, index, currentFileInfo, mapLines) {\n    this.value = string.value || string;\n    this.index = index;\n    this.mapLines = mapLines;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Anonymous.prototype = {\n    type: \"Anonymous\",\n    eval: function () { \n        return new tree.Anonymous(this.value, this.index, this.currentFileInfo, this.mapLines);\n    },\n    compare: function (x) {\n        if (!x.toCSS) {\n            return -1;\n        }\n        \n        var left = this.toCSS(),\n            right = x.toCSS();\n        \n        if (left === right) {\n            return 0;\n        }\n        \n        return left < right ? -1 : 1;\n    },\n    genCSS: function (env, output) {\n        output.add(this.value, this.currentFileInfo, this.index, this.mapLines);\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Assignment = function (key, val) {\n    this.key = key;\n    this.value = val;\n};\ntree.Assignment.prototype = {\n    type: \"Assignment\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        if (this.value.eval) {\n            return new(tree.Assignment)(this.key, this.value.eval(env));\n        }\n        return this;\n    },\n    genCSS: function (env, output) {\n        output.add(this.key + '=');\n        if (this.value.genCSS) {\n            this.value.genCSS(env, output);\n        } else {\n            output.add(this.value);\n        }\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\n//\n// A function call node.\n//\ntree.Call = function (name, args, index, currentFileInfo) {\n    this.name = name;\n    this.args = args;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Call.prototype = {\n    type: \"Call\",\n    accept: function (visitor) {\n        if (this.args) {\n            this.args = visitor.visitArray(this.args);\n        }\n    },\n    //\n    // When evaluating a function call,\n    // we either find the function in `tree.functions` [1],\n    // in which case we call it, passing the  evaluated arguments,\n    // if this returns null or we cannot find the function, we \n    // simply print it out as it appeared originally [2].\n    //\n    // The *functions.js* file contains the built-in functions.\n    //\n    // The reason why we evaluate the arguments, is in the case where\n    // we try to pass a variable to a function, like: `saturate(@color)`.\n    // The function should receive the value, not the variable.\n    //\n    eval: function (env) {\n        var args = this.args.map(function (a) { return a.eval(env); }),\n            nameLC = this.name.toLowerCase(),\n            result, func;\n\n        if (nameLC in tree.functions) { // 1.\n            try {\n                func = new tree.functionCall(env, this.currentFileInfo);\n                result = func[nameLC].apply(func, args);\n                if (result != null) {\n                    return result;\n                }\n            } catch (e) {\n                throw { type: e.type || \"Runtime\",\n                        message: \"error evaluating function `\" + this.name + \"`\" +\n                                 (e.message ? ': ' + e.message : ''),\n                        index: this.index, filename: this.currentFileInfo.filename };\n            }\n        }\n\n        return new tree.Call(this.name, args, this.index, this.currentFileInfo);\n    },\n\n    genCSS: function (env, output) {\n        output.add(this.name + \"(\", this.currentFileInfo, this.index);\n\n        for(var i = 0; i < this.args.length; i++) {\n            this.args[i].genCSS(env, output);\n            if (i + 1 < this.args.length) {\n                output.add(\", \");\n            }\n        }\n\n        output.add(\")\");\n    },\n\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n//\n// RGB Colors - #ff0014, #eee\n//\ntree.Color = function (rgb, a) {\n    //\n    // The end goal here, is to parse the arguments\n    // into an integer triplet, such as `128, 255, 0`\n    //\n    // This facilitates operations and conversions.\n    //\n    if (Array.isArray(rgb)) {\n        this.rgb = rgb;\n    } else if (rgb.length == 6) {\n        this.rgb = rgb.match(/.{2}/g).map(function (c) {\n            return parseInt(c, 16);\n        });\n    } else {\n        this.rgb = rgb.split('').map(function (c) {\n            return parseInt(c + c, 16);\n        });\n    }\n    this.alpha = typeof(a) === 'number' ? a : 1;\n};\n\nvar transparentKeyword = \"transparent\";\n\ntree.Color.prototype = {\n    type: \"Color\",\n    eval: function () { return this; },\n    luma: function () {\n        var r = this.rgb[0] / 255,\n            g = this.rgb[1] / 255,\n            b = this.rgb[2] / 255;\n\n        r = (r <= 0.03928) ? r / 12.92 : Math.pow(((r + 0.055) / 1.055), 2.4);\n        g = (g <= 0.03928) ? g / 12.92 : Math.pow(((g + 0.055) / 1.055), 2.4);\n        b = (b <= 0.03928) ? b / 12.92 : Math.pow(((b + 0.055) / 1.055), 2.4);\n\n        return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n    },\n\n    genCSS: function (env, output) {\n        output.add(this.toCSS(env));\n    },\n    toCSS: function (env, doNotCompress) {\n        var compress = env && env.compress && !doNotCompress,\n            alpha = tree.fround(env, this.alpha);\n\n        // If we have some transparency, the only way to represent it\n        // is via `rgba`. Otherwise, we use the hex representation,\n        // which has better compatibility with older browsers.\n        // Values are capped between `0` and `255`, rounded and zero-padded.\n        if (alpha < 1) {\n            if (alpha === 0 && this.isTransparentKeyword) {\n                return transparentKeyword;\n            }\n            return \"rgba(\" + this.rgb.map(function (c) {\n                return clamp(Math.round(c), 255);\n            }).concat(clamp(alpha, 1))\n                .join(',' + (compress ? '' : ' ')) + \")\";\n        } else {\n            var color = this.toRGB();\n\n            if (compress) {\n                var splitcolor = color.split('');\n\n                // Convert color to short format\n                if (splitcolor[1] === splitcolor[2] && splitcolor[3] === splitcolor[4] && splitcolor[5] === splitcolor[6]) {\n                    color = '#' + splitcolor[1] + splitcolor[3] + splitcolor[5];\n                }\n            }\n\n            return color;\n        }\n    },\n\n    //\n    // Operations have to be done per-channel, if not,\n    // channels will spill onto each other. Once we have\n    // our result, in the form of an integer triplet,\n    // we create a new Color node to hold the result.\n    //\n    operate: function (env, op, other) {\n        var rgb = [];\n        var alpha = this.alpha * (1 - other.alpha) + other.alpha;\n        for (var c = 0; c < 3; c++) {\n            rgb[c] = tree.operate(env, op, this.rgb[c], other.rgb[c]);\n        }\n        return new(tree.Color)(rgb, alpha);\n    },\n\n    toRGB: function () {\n        return toHex(this.rgb);\n    },\n\n    toHSL: function () {\n        var r = this.rgb[0] / 255,\n            g = this.rgb[1] / 255,\n            b = this.rgb[2] / 255,\n            a = this.alpha;\n\n        var max = Math.max(r, g, b), min = Math.min(r, g, b);\n        var h, s, l = (max + min) / 2, d = max - min;\n\n        if (max === min) {\n            h = s = 0;\n        } else {\n            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n\n            switch (max) {\n                case r: h = (g - b) / d + (g < b ? 6 : 0); break;\n                case g: h = (b - r) / d + 2;               break;\n                case b: h = (r - g) / d + 4;               break;\n            }\n            h /= 6;\n        }\n        return { h: h * 360, s: s, l: l, a: a };\n    },\n    //Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript\n    toHSV: function () {\n        var r = this.rgb[0] / 255,\n            g = this.rgb[1] / 255,\n            b = this.rgb[2] / 255,\n            a = this.alpha;\n\n        var max = Math.max(r, g, b), min = Math.min(r, g, b);\n        var h, s, v = max;\n\n        var d = max - min;\n        if (max === 0) {\n            s = 0;\n        } else {\n            s = d / max;\n        }\n\n        if (max === min) {\n            h = 0;\n        } else {\n            switch(max){\n                case r: h = (g - b) / d + (g < b ? 6 : 0); break;\n                case g: h = (b - r) / d + 2; break;\n                case b: h = (r - g) / d + 4; break;\n            }\n            h /= 6;\n        }\n        return { h: h * 360, s: s, v: v, a: a };\n    },\n    toARGB: function () {\n        return toHex([this.alpha * 255].concat(this.rgb));\n    },\n    compare: function (x) {\n        if (!x.rgb) {\n            return -1;\n        }\n        \n        return (x.rgb[0] === this.rgb[0] &&\n            x.rgb[1] === this.rgb[1] &&\n            x.rgb[2] === this.rgb[2] &&\n            x.alpha === this.alpha) ? 0 : -1;\n    }\n};\n\ntree.Color.fromKeyword = function(keyword) {\n    keyword = keyword.toLowerCase();\n\n    if (tree.colors.hasOwnProperty(keyword)) {\n        // detect named color\n        return new(tree.Color)(tree.colors[keyword].slice(1));\n    }\n    if (keyword === transparentKeyword) {\n        var transparent = new(tree.Color)([0, 0, 0], 0);\n        transparent.isTransparentKeyword = true;\n        return transparent;\n    }\n};\n\nfunction toHex(v) {\n    return '#' + v.map(function (c) {\n        c = clamp(Math.round(c), 255);\n        return (c < 16 ? '0' : '') + c.toString(16);\n    }).join('');\n}\n\nfunction clamp(v, max) {\n    return Math.min(Math.max(v, 0), max); \n}\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Comment = function (value, silent, index, currentFileInfo) {\n    this.value = value;\n    this.silent = !!silent;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Comment.prototype = {\n    type: \"Comment\",\n    genCSS: function (env, output) {\n        if (this.debugInfo) {\n            output.add(tree.debugInfo(env, this), this.currentFileInfo, this.index);\n        }\n        output.add(this.value.trim()); //TODO shouldn't need to trim, we shouldn't grab the \\n\n    },\n    toCSS: tree.toCSS,\n    isSilent: function(env) {\n        var isReference = (this.currentFileInfo && this.currentFileInfo.reference && !this.isReferenced),\n            isCompressed = env.compress && !this.value.match(/^\\/\\*!/);\n        return this.silent || isReference || isCompressed;\n    },\n    eval: function () { return this; },\n    markReferenced: function () {\n        this.isReferenced = true;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Condition = function (op, l, r, i, negate) {\n    this.op = op.trim();\n    this.lvalue = l;\n    this.rvalue = r;\n    this.index = i;\n    this.negate = negate;\n};\ntree.Condition.prototype = {\n    type: \"Condition\",\n    accept: function (visitor) {\n        this.lvalue = visitor.visit(this.lvalue);\n        this.rvalue = visitor.visit(this.rvalue);\n    },\n    eval: function (env) {\n        var a = this.lvalue.eval(env),\n            b = this.rvalue.eval(env);\n\n        var i = this.index, result;\n\n        result = (function (op) {\n            switch (op) {\n                case 'and':\n                    return a && b;\n                case 'or':\n                    return a || b;\n                default:\n                    if (a.compare) {\n                        result = a.compare(b);\n                    } else if (b.compare) {\n                        result = b.compare(a);\n                    } else {\n                        throw { type: \"Type\",\n                                message: \"Unable to perform comparison\",\n                                index: i };\n                    }\n                    switch (result) {\n                        case -1: return op === '<' || op === '=<' || op === '<=';\n                        case  0: return op === '=' || op === '>=' || op === '=<' || op === '<=';\n                        case  1: return op === '>' || op === '>=';\n                    }\n            }\n        })(this.op);\n        return this.negate ? !result : result;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.DetachedRuleset = function (ruleset, frames) {\n    this.ruleset = ruleset;\n    this.frames = frames;\n};\ntree.DetachedRuleset.prototype = {\n    type: \"DetachedRuleset\",\n    accept: function (visitor) {\n        this.ruleset = visitor.visit(this.ruleset);\n    },\n    eval: function (env) {\n        var frames = this.frames || env.frames.slice(0);\n        return new tree.DetachedRuleset(this.ruleset, frames);\n    },\n    callEval: function (env) {\n        return this.ruleset.eval(this.frames ? new(tree.evalEnv)(env, this.frames.concat(env.frames)) : env);\n    }\n};\n})(require('../tree'));\n\n(function (tree) {\n\n//\n// A number with a unit\n//\ntree.Dimension = function (value, unit) {\n    this.value = parseFloat(value);\n    this.unit = (unit && unit instanceof tree.Unit) ? unit :\n      new(tree.Unit)(unit ? [unit] : undefined);\n};\n\ntree.Dimension.prototype = {\n    type: \"Dimension\",\n    accept: function (visitor) {\n        this.unit = visitor.visit(this.unit);\n    },\n    eval: function (env) {\n        return this;\n    },\n    toColor: function () {\n        return new(tree.Color)([this.value, this.value, this.value]);\n    },\n    genCSS: function (env, output) {\n        if ((env && env.strictUnits) && !this.unit.isSingular()) {\n            throw new Error(\"Multiple units in dimension. Correct the units or use the unit function. Bad unit: \"+this.unit.toString());\n        }\n\n        var value = tree.fround(env, this.value),\n            strValue = String(value);\n\n        if (value !== 0 && value < 0.000001 && value > -0.000001) {\n            // would be output 1e-6 etc.\n            strValue = value.toFixed(20).replace(/0+$/, \"\");\n        }\n\n        if (env && env.compress) {\n            // Zero values doesn't need a unit\n            if (value === 0 && this.unit.isLength()) {\n                output.add(strValue);\n                return;\n            }\n\n            // Float values doesn't need a leading zero\n            if (value > 0 && value < 1) {\n                strValue = (strValue).substr(1);\n            }\n        }\n\n        output.add(strValue);\n        this.unit.genCSS(env, output);\n    },\n    toCSS: tree.toCSS,\n\n    // In an operation between two Dimensions,\n    // we default to the first Dimension's unit,\n    // so `1px + 2` will yield `3px`.\n    operate: function (env, op, other) {\n        /*jshint noempty:false */\n        var value = tree.operate(env, op, this.value, other.value),\n            unit = this.unit.clone();\n\n        if (op === '+' || op === '-') {\n            if (unit.numerator.length === 0 && unit.denominator.length === 0) {\n                unit.numerator = other.unit.numerator.slice(0);\n                unit.denominator = other.unit.denominator.slice(0);\n            } else if (other.unit.numerator.length === 0 && unit.denominator.length === 0) {\n                // do nothing\n            } else {\n                other = other.convertTo(this.unit.usedUnits());\n\n                if(env.strictUnits && other.unit.toString() !== unit.toString()) {\n                  throw new Error(\"Incompatible units. Change the units or use the unit function. Bad units: '\" + unit.toString() +\n                    \"' and '\" + other.unit.toString() + \"'.\");\n                }\n\n                value = tree.operate(env, op, this.value, other.value);\n            }\n        } else if (op === '*') {\n            unit.numerator = unit.numerator.concat(other.unit.numerator).sort();\n            unit.denominator = unit.denominator.concat(other.unit.denominator).sort();\n            unit.cancel();\n        } else if (op === '/') {\n            unit.numerator = unit.numerator.concat(other.unit.denominator).sort();\n            unit.denominator = unit.denominator.concat(other.unit.numerator).sort();\n            unit.cancel();\n        }\n        return new(tree.Dimension)(value, unit);\n    },\n\n    compare: function (other) {\n        if (other instanceof tree.Dimension) {\n            var a, b,\n                aValue, bValue;\n            \n            if (this.unit.isEmpty() || other.unit.isEmpty()) {\n                a = this;\n                b = other;\n            } else {\n                a = this.unify();\n                b = other.unify();\n                if (a.unit.compare(b.unit) !== 0) {\n                    return -1;\n                }                \n            }\n            aValue = a.value;\n            bValue = b.value;\n\n            if (bValue > aValue) {\n                return -1;\n            } else if (bValue < aValue) {\n                return 1;\n            } else {\n                return 0;\n            }\n        } else {\n            return -1;\n        }\n    },\n\n    unify: function () {\n        return this.convertTo({ length: 'px', duration: 's', angle: 'rad' });\n    },\n\n    convertTo: function (conversions) {\n        var value = this.value, unit = this.unit.clone(),\n            i, groupName, group, targetUnit, derivedConversions = {}, applyUnit;\n\n        if (typeof conversions === 'string') {\n            for(i in tree.UnitConversions) {\n                if (tree.UnitConversions[i].hasOwnProperty(conversions)) {\n                    derivedConversions = {};\n                    derivedConversions[i] = conversions;\n                }\n            }\n            conversions = derivedConversions;\n        }\n        applyUnit = function (atomicUnit, denominator) {\n          /*jshint loopfunc:true */\n            if (group.hasOwnProperty(atomicUnit)) {\n                if (denominator) {\n                    value = value / (group[atomicUnit] / group[targetUnit]);\n                } else {\n                    value = value * (group[atomicUnit] / group[targetUnit]);\n                }\n\n                return targetUnit;\n            }\n\n            return atomicUnit;\n        };\n\n        for (groupName in conversions) {\n            if (conversions.hasOwnProperty(groupName)) {\n                targetUnit = conversions[groupName];\n                group = tree.UnitConversions[groupName];\n\n                unit.map(applyUnit);\n            }\n        }\n\n        unit.cancel();\n\n        return new(tree.Dimension)(value, unit);\n    }\n};\n\n// http://www.w3.org/TR/css3-values/#absolute-lengths\ntree.UnitConversions = {\n    length: {\n         'm': 1,\n        'cm': 0.01,\n        'mm': 0.001,\n        'in': 0.0254,\n        'px': 0.0254 / 96,\n        'pt': 0.0254 / 72,\n        'pc': 0.0254 / 72 * 12\n    },\n    duration: {\n        's': 1,\n        'ms': 0.001\n    },\n    angle: {\n        'rad': 1/(2*Math.PI),\n        'deg': 1/360,\n        'grad': 1/400,\n        'turn': 1\n    }\n};\n\ntree.Unit = function (numerator, denominator, backupUnit) {\n    this.numerator = numerator ? numerator.slice(0).sort() : [];\n    this.denominator = denominator ? denominator.slice(0).sort() : [];\n    this.backupUnit = backupUnit;\n};\n\ntree.Unit.prototype = {\n    type: \"Unit\",\n    clone: function () {\n        return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0), this.backupUnit);\n    },\n    genCSS: function (env, output) {\n        if (this.numerator.length >= 1) {\n            output.add(this.numerator[0]);\n        } else\n        if (this.denominator.length >= 1) {\n            output.add(this.denominator[0]);\n        } else\n        if ((!env || !env.strictUnits) && this.backupUnit) {\n            output.add(this.backupUnit);\n        }\n    },\n    toCSS: tree.toCSS,\n\n    toString: function () {\n      var i, returnStr = this.numerator.join(\"*\");\n      for (i = 0; i < this.denominator.length; i++) {\n          returnStr += \"/\" + this.denominator[i];\n      }\n      return returnStr;\n    },\n\n    compare: function (other) {\n        return this.is(other.toString()) ? 0 : -1;\n    },\n\n    is: function (unitString) {\n        return this.toString() === unitString;\n    },\n\n    isLength: function () {\n        return Boolean(this.toCSS().match(/px|em|%|in|cm|mm|pc|pt|ex/));\n    },\n\n    isEmpty: function () {\n        return this.numerator.length === 0 && this.denominator.length === 0;\n    },\n\n    isSingular: function() {\n        return this.numerator.length <= 1 && this.denominator.length === 0;\n    },\n\n    map: function(callback) {\n        var i;\n\n        for (i = 0; i < this.numerator.length; i++) {\n            this.numerator[i] = callback(this.numerator[i], false);\n        }\n\n        for (i = 0; i < this.denominator.length; i++) {\n            this.denominator[i] = callback(this.denominator[i], true);\n        }\n    },\n\n    usedUnits: function() {\n        var group, result = {}, mapUnit;\n\n        mapUnit = function (atomicUnit) {\n        /*jshint loopfunc:true */\n            if (group.hasOwnProperty(atomicUnit) && !result[groupName]) {\n                result[groupName] = atomicUnit;\n            }\n\n            return atomicUnit;\n        };\n\n        for (var groupName in tree.UnitConversions) {\n            if (tree.UnitConversions.hasOwnProperty(groupName)) {\n                group = tree.UnitConversions[groupName];\n\n                this.map(mapUnit);\n            }\n        }\n\n        return result;\n    },\n\n    cancel: function () {\n        var counter = {}, atomicUnit, i, backup;\n\n        for (i = 0; i < this.numerator.length; i++) {\n            atomicUnit = this.numerator[i];\n            if (!backup) {\n                backup = atomicUnit;\n            }\n            counter[atomicUnit] = (counter[atomicUnit] || 0) + 1;\n        }\n\n        for (i = 0; i < this.denominator.length; i++) {\n            atomicUnit = this.denominator[i];\n            if (!backup) {\n                backup = atomicUnit;\n            }\n            counter[atomicUnit] = (counter[atomicUnit] || 0) - 1;\n        }\n\n        this.numerator = [];\n        this.denominator = [];\n\n        for (atomicUnit in counter) {\n            if (counter.hasOwnProperty(atomicUnit)) {\n                var count = counter[atomicUnit];\n\n                if (count > 0) {\n                    for (i = 0; i < count; i++) {\n                        this.numerator.push(atomicUnit);\n                    }\n                } else if (count < 0) {\n                    for (i = 0; i < -count; i++) {\n                        this.denominator.push(atomicUnit);\n                    }\n                }\n            }\n        }\n\n        if (this.numerator.length === 0 && this.denominator.length === 0 && backup) {\n            this.backupUnit = backup;\n        }\n\n        this.numerator.sort();\n        this.denominator.sort();\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Directive = function (name, value, rules, index, currentFileInfo, debugInfo) {\n    this.name  = name;\n    this.value = value;\n    if (rules) {\n        this.rules = rules;\n        this.rules.allowImports = true;\n    }\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n    this.debugInfo = debugInfo;\n};\n\ntree.Directive.prototype = {\n    type: \"Directive\",\n    accept: function (visitor) {\n        var value = this.value, rules = this.rules;\n        if (rules) {\n            rules = visitor.visit(rules);\n        }\n        if (value) {\n            value = visitor.visit(value);\n        }\n    },\n    genCSS: function (env, output) {\n        var value = this.value, rules = this.rules;\n        output.add(this.name, this.currentFileInfo, this.index);\n        if (value) {\n            output.add(' ');\n            value.genCSS(env, output);\n        }\n        if (rules) {\n            tree.outputRuleset(env, output, [rules]);\n        } else {\n            output.add(';');\n        }\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        var value = this.value, rules = this.rules;\n        if (value) {\n            value = value.eval(env);\n        }\n        if (rules) {\n            rules = rules.eval(env);\n            rules.root = true;\n        }\n        return new(tree.Directive)(this.name, value, rules,\n            this.index, this.currentFileInfo, this.debugInfo);\n    },\n    variable: function (name) { if (this.rules) return tree.Ruleset.prototype.variable.call(this.rules, name); },\n    find: function () { if (this.rules) return tree.Ruleset.prototype.find.apply(this.rules, arguments); },\n    rulesets: function () { if (this.rules) return tree.Ruleset.prototype.rulesets.apply(this.rules); },\n    markReferenced: function () {\n        var i, rules;\n        this.isReferenced = true;\n        if (this.rules) {\n            rules = this.rules.rules;\n            for (i = 0; i < rules.length; i++) {\n                if (rules[i].markReferenced) {\n                    rules[i].markReferenced();\n                }\n            }\n        }\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Element = function (combinator, value, index, currentFileInfo) {\n    this.combinator = combinator instanceof tree.Combinator ?\n                      combinator : new(tree.Combinator)(combinator);\n\n    if (typeof(value) === 'string') {\n        this.value = value.trim();\n    } else if (value) {\n        this.value = value;\n    } else {\n        this.value = \"\";\n    }\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Element.prototype = {\n    type: \"Element\",\n    accept: function (visitor) {\n        var value = this.value;\n        this.combinator = visitor.visit(this.combinator);\n        if (typeof value === \"object\") {\n            this.value = visitor.visit(value);\n        }\n    },\n    eval: function (env) {\n        return new(tree.Element)(this.combinator,\n                                 this.value.eval ? this.value.eval(env) : this.value,\n                                 this.index,\n                                 this.currentFileInfo);\n    },\n    genCSS: function (env, output) {\n        output.add(this.toCSS(env), this.currentFileInfo, this.index);\n    },\n    toCSS: function (env) {\n        var value = (this.value.toCSS ? this.value.toCSS(env) : this.value);\n        if (value === '' && this.combinator.value.charAt(0) === '&') {\n            return '';\n        } else {\n            return this.combinator.toCSS(env || {}) + value;\n        }\n    }\n};\n\ntree.Attribute = function (key, op, value) {\n    this.key = key;\n    this.op = op;\n    this.value = value;\n};\ntree.Attribute.prototype = {\n    type: \"Attribute\",\n    eval: function (env) {\n        return new(tree.Attribute)(this.key.eval ? this.key.eval(env) : this.key,\n            this.op, (this.value && this.value.eval) ? this.value.eval(env) : this.value);\n    },\n    genCSS: function (env, output) {\n        output.add(this.toCSS(env));\n    },\n    toCSS: function (env) {\n        var value = this.key.toCSS ? this.key.toCSS(env) : this.key;\n\n        if (this.op) {\n            value += this.op;\n            value += (this.value.toCSS ? this.value.toCSS(env) : this.value);\n        }\n\n        return '[' + value + ']';\n    }\n};\n\ntree.Combinator = function (value) {\n    if (value === ' ') {\n        this.value = ' ';\n    } else {\n        this.value = value ? value.trim() : \"\";\n    }\n};\ntree.Combinator.prototype = {\n    type: \"Combinator\",\n    _outputMap: {\n        ''  : '',\n        ' ' : ' ',\n        ':' : ' :',\n        '+' : ' + ',\n        '~' : ' ~ ',\n        '>' : ' > ',\n        '|' : '|',\n        '^' : ' ^ ',\n        '^^' : ' ^^ '\n    },\n    _outputMapCompressed: {\n        ''  : '',\n        ' ' : ' ',\n        ':' : ' :',\n        '+' : '+',\n        '~' : '~',\n        '>' : '>',\n        '|' : '|',\n        '^' : '^',\n        '^^' : '^^'\n    },\n    genCSS: function (env, output) {\n        output.add((env.compress ? this._outputMapCompressed : this._outputMap)[this.value]);\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Expression = function (value) { this.value = value; };\ntree.Expression.prototype = {\n    type: \"Expression\",\n    accept: function (visitor) {\n        if (this.value) {\n            this.value = visitor.visitArray(this.value);\n        }\n    },\n    eval: function (env) {\n        var returnValue,\n            inParenthesis = this.parens && !this.parensInOp,\n            doubleParen = false;\n        if (inParenthesis) {\n            env.inParenthesis();\n        }\n        if (this.value.length > 1) {\n            returnValue = new(tree.Expression)(this.value.map(function (e) {\n                return e.eval(env);\n            }));\n        } else if (this.value.length === 1) {\n            if (this.value[0].parens && !this.value[0].parensInOp) {\n                doubleParen = true;\n            }\n            returnValue = this.value[0].eval(env);\n        } else {\n            returnValue = this;\n        }\n        if (inParenthesis) {\n            env.outOfParenthesis();\n        }\n        if (this.parens && this.parensInOp && !(env.isMathOn()) && !doubleParen) {\n            returnValue = new(tree.Paren)(returnValue);\n        }\n        return returnValue;\n    },\n    genCSS: function (env, output) {\n        for(var i = 0; i < this.value.length; i++) {\n            this.value[i].genCSS(env, output);\n            if (i + 1 < this.value.length) {\n                output.add(\" \");\n            }\n        }\n    },\n    toCSS: tree.toCSS,\n    throwAwayComments: function () {\n        this.value = this.value.filter(function(v) {\n            return !(v instanceof tree.Comment);\n        });\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Extend = function Extend(selector, option, index) {\n    this.selector = selector;\n    this.option = option;\n    this.index = index;\n    this.object_id = tree.Extend.next_id++;\n    this.parent_ids = [this.object_id];\n\n    switch(option) {\n        case \"all\":\n            this.allowBefore = true;\n            this.allowAfter = true;\n        break;\n        default:\n            this.allowBefore = false;\n            this.allowAfter = false;\n        break;\n    }\n};\ntree.Extend.next_id = 0;\n\ntree.Extend.prototype = {\n    type: \"Extend\",\n    accept: function (visitor) {\n        this.selector = visitor.visit(this.selector);\n    },\n    eval: function (env) {\n        return new(tree.Extend)(this.selector.eval(env), this.option, this.index);\n    },\n    clone: function (env) {\n        return new(tree.Extend)(this.selector, this.option, this.index);\n    },\n    findSelfSelectors: function (selectors) {\n        var selfElements = [],\n            i,\n            selectorElements;\n\n        for(i = 0; i < selectors.length; i++) {\n            selectorElements = selectors[i].elements;\n            // duplicate the logic in genCSS function inside the selector node.\n            // future TODO - move both logics into the selector joiner visitor\n            if (i > 0 && selectorElements.length && selectorElements[0].combinator.value === \"\") {\n                selectorElements[0].combinator.value = ' ';\n            }\n            selfElements = selfElements.concat(selectors[i].elements);\n        }\n\n        this.selfSelectors = [{ elements: selfElements }];\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n//\n// CSS @import node\n//\n// The general strategy here is that we don't want to wait\n// for the parsing to be completed, before we start importing\n// the file. That's because in the context of a browser,\n// most of the time will be spent waiting for the server to respond.\n//\n// On creation, we push the import path to our import queue, though\n// `import,push`, we also pass it a callback, which it'll call once\n// the file has been fetched, and parsed.\n//\ntree.Import = function (path, features, options, index, currentFileInfo) {\n    this.options = options;\n    this.index = index;\n    this.path = path;\n    this.features = features;\n    this.currentFileInfo = currentFileInfo;\n\n    if (this.options.less !== undefined || this.options.inline) {\n        this.css = !this.options.less || this.options.inline;\n    } else {\n        var pathValue = this.getPath();\n        if (pathValue && /css([\\?;].*)?$/.test(pathValue)) {\n            this.css = true;\n        }\n    }\n};\n\n//\n// The actual import node doesn't return anything, when converted to CSS.\n// The reason is that it's used at the evaluation stage, so that the rules\n// it imports can be treated like any other rules.\n//\n// In `eval`, we make sure all Import nodes get evaluated, recursively, so\n// we end up with a flat structure, which can easily be imported in the parent\n// ruleset.\n//\ntree.Import.prototype = {\n    type: \"Import\",\n    accept: function (visitor) {\n        if (this.features) {\n            this.features = visitor.visit(this.features);\n        }\n        this.path = visitor.visit(this.path);\n        if (!this.options.inline && this.root) {\n            this.root = visitor.visit(this.root);\n        }\n    },\n    genCSS: function (env, output) {\n        if (this.css) {\n            output.add(\"@import \", this.currentFileInfo, this.index);\n            this.path.genCSS(env, output);\n            if (this.features) {\n                output.add(\" \");\n                this.features.genCSS(env, output);\n            }\n            output.add(';');\n        }\n    },\n    toCSS: tree.toCSS,\n    getPath: function () {\n        if (this.path instanceof tree.Quoted) {\n            var path = this.path.value;\n            return (this.css !== undefined || /(\\.[a-z]*$)|([\\?;].*)$/.test(path)) ? path : path + '.less';\n        } else if (this.path instanceof tree.URL) {\n            return this.path.value.value;\n        }\n        return null;\n    },\n    evalForImport: function (env) {\n        return new(tree.Import)(this.path.eval(env), this.features, this.options, this.index, this.currentFileInfo);\n    },\n    evalPath: function (env) {\n        var path = this.path.eval(env);\n        var rootpath = this.currentFileInfo && this.currentFileInfo.rootpath;\n\n        if (!(path instanceof tree.URL)) {\n            if (rootpath) {\n                var pathValue = path.value;\n                // Add the base path if the import is relative\n                if (pathValue && env.isPathRelative(pathValue)) {\n                    path.value = rootpath +pathValue;\n                }\n            }\n            path.value = env.normalizePath(path.value);\n        }\n\n        return path;\n    },\n    eval: function (env) {\n        var ruleset, features = this.features && this.features.eval(env);\n\n        if (this.skip) {\n            if (typeof this.skip === \"function\") {\n                this.skip = this.skip();\n            }\n            if (this.skip) {\n                return []; \n            }\n        }\n         \n        if (this.options.inline) {\n            //todo needs to reference css file not import\n            var contents = new(tree.Anonymous)(this.root, 0, {filename: this.importedFilename}, true);\n            return this.features ? new(tree.Media)([contents], this.features.value) : [contents];\n        } else if (this.css) {\n            var newImport = new(tree.Import)(this.evalPath(env), features, this.options, this.index);\n            if (!newImport.css && this.error) {\n                throw this.error;\n            }\n            return newImport;\n        } else {\n            ruleset = new(tree.Ruleset)(null, this.root.rules.slice(0));\n\n            ruleset.evalImports(env);\n\n            return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules;\n        }\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.JavaScript = function (string, index, escaped) {\n    this.escaped = escaped;\n    this.expression = string;\n    this.index = index;\n};\ntree.JavaScript.prototype = {\n    type: \"JavaScript\",\n    eval: function (env) {\n        var result,\n            that = this,\n            context = {};\n\n        var expression = this.expression.replace(/@\\{([\\w-]+)\\}/g, function (_, name) {\n            return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env));\n        });\n\n        try {\n            expression = new(Function)('return (' + expression + ')');\n        } catch (e) {\n            throw { message: \"JavaScript evaluation error: \" + e.message + \" from `\" + expression + \"`\" ,\n                    index: this.index };\n        }\n\n        var variables = env.frames[0].variables();\n        for (var k in variables) {\n            if (variables.hasOwnProperty(k)) {\n                /*jshint loopfunc:true */\n                context[k.slice(1)] = {\n                    value: variables[k].value,\n                    toJS: function () {\n                        return this.value.eval(env).toCSS();\n                    }\n                };\n            }\n        }\n\n        try {\n            result = expression.call(context);\n        } catch (e) {\n            throw { message: \"JavaScript evaluation error: '\" + e.name + ': ' + e.message.replace(/[\"]/g, \"'\") + \"'\" ,\n                    index: this.index };\n        }\n        if (typeof(result) === 'number') {\n            return new(tree.Dimension)(result);\n        } else if (typeof(result) === 'string') {\n            return new(tree.Quoted)('\"' + result + '\"', result, this.escaped, this.index);\n        } else if (Array.isArray(result)) {\n            return new(tree.Anonymous)(result.join(', '));\n        } else {\n            return new(tree.Anonymous)(result);\n        }\n    }\n};\n\n})(require('../tree'));\n\n\n(function (tree) {\n\ntree.Keyword = function (value) { this.value = value; };\ntree.Keyword.prototype = {\n    type: \"Keyword\",\n    eval: function () { return this; },\n    genCSS: function (env, output) {\n        if (this.value === '%') { throw { type: \"Syntax\", message: \"Invalid % without number\" }; }\n        output.add(this.value);\n    },\n    toCSS: tree.toCSS,\n    compare: function (other) {\n        if (other instanceof tree.Keyword) {\n            return other.value === this.value ? 0 : 1;\n        } else {\n            return -1;\n        }\n    }\n};\n\ntree.True = new(tree.Keyword)('true');\ntree.False = new(tree.Keyword)('false');\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Media = function (value, features, index, currentFileInfo) {\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n\n    var selectors = this.emptySelectors();\n\n    this.features = new(tree.Value)(features);\n    this.rules = [new(tree.Ruleset)(selectors, value)];\n    this.rules[0].allowImports = true;\n};\ntree.Media.prototype = {\n    type: \"Media\",\n    accept: function (visitor) {\n        if (this.features) {\n            this.features = visitor.visit(this.features);\n        }\n        if (this.rules) {\n            this.rules = visitor.visitArray(this.rules);\n        }\n    },\n    genCSS: function (env, output) {\n        output.add('@media ', this.currentFileInfo, this.index);\n        this.features.genCSS(env, output);\n        tree.outputRuleset(env, output, this.rules);\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        if (!env.mediaBlocks) {\n            env.mediaBlocks = [];\n            env.mediaPath = [];\n        }\n        \n        var media = new(tree.Media)(null, [], this.index, this.currentFileInfo);\n        if(this.debugInfo) {\n            this.rules[0].debugInfo = this.debugInfo;\n            media.debugInfo = this.debugInfo;\n        }\n        var strictMathBypass = false;\n        if (!env.strictMath) {\n            strictMathBypass = true;\n            env.strictMath = true;\n        }\n        try {\n            media.features = this.features.eval(env);\n        }\n        finally {\n            if (strictMathBypass) {\n                env.strictMath = false;\n            }\n        }\n        \n        env.mediaPath.push(media);\n        env.mediaBlocks.push(media);\n        \n        env.frames.unshift(this.rules[0]);\n        media.rules = [this.rules[0].eval(env)];\n        env.frames.shift();\n        \n        env.mediaPath.pop();\n\n        return env.mediaPath.length === 0 ? media.evalTop(env) :\n                    media.evalNested(env);\n    },\n    variable: function (name) { return tree.Ruleset.prototype.variable.call(this.rules[0], name); },\n    find: function () { return tree.Ruleset.prototype.find.apply(this.rules[0], arguments); },\n    rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.rules[0]); },\n    emptySelectors: function() { \n        var el = new(tree.Element)('', '&', this.index, this.currentFileInfo),\n            sels = [new(tree.Selector)([el], null, null, this.index, this.currentFileInfo)];\n        sels[0].mediaEmpty = true;\n        return sels;\n    },\n    markReferenced: function () {\n        var i, rules = this.rules[0].rules;\n        this.rules[0].markReferenced();\n        this.isReferenced = true;\n        for (i = 0; i < rules.length; i++) {\n            if (rules[i].markReferenced) {\n                rules[i].markReferenced();\n            }\n        }\n    },\n\n    evalTop: function (env) {\n        var result = this;\n\n        // Render all dependent Media blocks.\n        if (env.mediaBlocks.length > 1) {\n            var selectors = this.emptySelectors();\n            result = new(tree.Ruleset)(selectors, env.mediaBlocks);\n            result.multiMedia = true;\n        }\n\n        delete env.mediaBlocks;\n        delete env.mediaPath;\n\n        return result;\n    },\n    evalNested: function (env) {\n        var i, value,\n            path = env.mediaPath.concat([this]);\n\n        // Extract the media-query conditions separated with `,` (OR).\n        for (i = 0; i < path.length; i++) {\n            value = path[i].features instanceof tree.Value ?\n                        path[i].features.value : path[i].features;\n            path[i] = Array.isArray(value) ? value : [value];\n        }\n\n        // Trace all permutations to generate the resulting media-query.\n        //\n        // (a, b and c) with nested (d, e) ->\n        //    a and d\n        //    a and e\n        //    b and c and d\n        //    b and c and e\n        this.features = new(tree.Value)(this.permute(path).map(function (path) {\n            path = path.map(function (fragment) {\n                return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment);\n            });\n\n            for(i = path.length - 1; i > 0; i--) {\n                path.splice(i, 0, new(tree.Anonymous)(\"and\"));\n            }\n\n            return new(tree.Expression)(path);\n        }));\n\n        // Fake a tree-node that doesn't output anything.\n        return new(tree.Ruleset)([], []);\n    },\n    permute: function (arr) {\n      if (arr.length === 0) {\n          return [];\n      } else if (arr.length === 1) {\n          return arr[0];\n      } else {\n          var result = [];\n          var rest = this.permute(arr.slice(1));\n          for (var i = 0; i < rest.length; i++) {\n              for (var j = 0; j < arr[0].length; j++) {\n                  result.push([arr[0][j]].concat(rest[i]));\n              }\n          }\n          return result;\n      }\n    },\n    bubbleSelectors: function (selectors) {\n      if (!selectors)\n        return;\n      this.rules = [new(tree.Ruleset)(selectors.slice(0), [this.rules[0]])];\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.mixin = {};\ntree.mixin.Call = function (elements, args, index, currentFileInfo, important) {\n    this.selector = new(tree.Selector)(elements);\n    this.arguments = (args && args.length) ? args : null;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n    this.important = important;\n};\ntree.mixin.Call.prototype = {\n    type: \"MixinCall\",\n    accept: function (visitor) {\n        if (this.selector) {\n            this.selector = visitor.visit(this.selector);\n        }\n        if (this.arguments) {\n            this.arguments = visitor.visitArray(this.arguments);\n        }\n    },\n    eval: function (env) {\n        var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound, rule,\n            candidates = [], candidate, conditionResult = [], defaultFunc = tree.defaultFunc,\n            defaultResult, defNone = 0, defTrue = 1, defFalse = 2, count; \n\n        args = this.arguments && this.arguments.map(function (a) {\n            return { name: a.name, value: a.value.eval(env) };\n        });\n\n        for (i = 0; i < env.frames.length; i++) {\n            if ((mixins = env.frames[i].find(this.selector)).length > 0) {\n                isOneFound = true;\n                \n                // To make `default()` function independent of definition order we have two \"subpasses\" here.\n                // At first we evaluate each guard *twice* (with `default() == true` and `default() == false`),\n                // and build candidate list with corresponding flags. Then, when we know all possible matches,\n                // we make a final decision.\n                \n                for (m = 0; m < mixins.length; m++) {\n                    mixin = mixins[m];\n                    isRecursive = false;\n                    for(f = 0; f < env.frames.length; f++) {\n                        if ((!(mixin instanceof tree.mixin.Definition)) && mixin === (env.frames[f].originalRuleset || env.frames[f])) {\n                            isRecursive = true;\n                            break;\n                        }\n                    }\n                    if (isRecursive) {\n                        continue;\n                    }\n                    \n                    if (mixin.matchArgs(args, env)) {  \n                        candidate = {mixin: mixin, group: defNone};\n                        \n                        if (mixin.matchCondition) { \n                            for (f = 0; f < 2; f++) {\n                                defaultFunc.value(f);\n                                conditionResult[f] = mixin.matchCondition(args, env);\n                            }\n                            if (conditionResult[0] || conditionResult[1]) {\n                                if (conditionResult[0] != conditionResult[1]) {\n                                    candidate.group = conditionResult[1] ?\n                                        defTrue : defFalse;\n                                }\n\n                                candidates.push(candidate);\n                            }   \n                        }\n                        else {\n                            candidates.push(candidate);\n                        }\n                        \n                        match = true;\n                    }\n                }\n                \n                defaultFunc.reset();\n\n                count = [0, 0, 0];\n                for (m = 0; m < candidates.length; m++) {\n                    count[candidates[m].group]++;\n                }\n\n                if (count[defNone] > 0) {\n                    defaultResult = defFalse;\n                } else {\n                    defaultResult = defTrue;\n                    if ((count[defTrue] + count[defFalse]) > 1) {\n                        throw { type: 'Runtime',\n                            message: 'Ambiguous use of `default()` found when matching for `'\n                                + this.format(args) + '`',\n                            index: this.index, filename: this.currentFileInfo.filename };\n                    }\n                }\n                \n                for (m = 0; m < candidates.length; m++) {\n                    candidate = candidates[m].group;\n                    if ((candidate === defNone) || (candidate === defaultResult)) {\n                        try {\n                            mixin = candidates[m].mixin;\n                            if (!(mixin instanceof tree.mixin.Definition)) {\n                                mixin = new tree.mixin.Definition(\"\", [], mixin.rules, null, false);\n                                mixin.originalRuleset = mixins[m].originalRuleset || mixins[m];\n                            }\n                            Array.prototype.push.apply(\n                                  rules, mixin.evalCall(env, args, this.important).rules);\n                        } catch (e) {\n                            throw { message: e.message, index: this.index, filename: this.currentFileInfo.filename, stack: e.stack };\n                        }\n                    }\n                }\n                \n                if (match) {\n                    if (!this.currentFileInfo || !this.currentFileInfo.reference) {\n                        for (i = 0; i < rules.length; i++) {\n                            rule = rules[i];\n                            if (rule.markReferenced) {\n                                rule.markReferenced();\n                            }\n                        }\n                    }\n                    return rules;\n                }\n            }\n        }\n        if (isOneFound) {\n            throw { type:    'Runtime',\n                    message: 'No matching definition was found for `' + this.format(args) + '`',\n                    index:   this.index, filename: this.currentFileInfo.filename };\n        } else {\n            throw { type:    'Name',\n                    message: this.selector.toCSS().trim() + \" is undefined\",\n                    index:   this.index, filename: this.currentFileInfo.filename };\n        }\n    },\n    format: function (args) {\n        return this.selector.toCSS().trim() + '(' +\n            (args ? args.map(function (a) {\n                var argValue = \"\";\n                if (a.name) {\n                    argValue += a.name + \":\";\n                }\n                if (a.value.toCSS) {\n                    argValue += a.value.toCSS();\n                } else {\n                    argValue += \"???\";\n                }\n                return argValue;\n            }).join(', ') : \"\") + \")\";\n    }\n};\n\ntree.mixin.Definition = function (name, params, rules, condition, variadic, frames) {\n    this.name = name;\n    this.selectors = [new(tree.Selector)([new(tree.Element)(null, name, this.index, this.currentFileInfo)])];\n    this.params = params;\n    this.condition = condition;\n    this.variadic = variadic;\n    this.arity = params.length;\n    this.rules = rules;\n    this._lookups = {};\n    this.required = params.reduce(function (count, p) {\n        if (!p.name || (p.name && !p.value)) { return count + 1; }\n        else                                 { return count; }\n    }, 0);\n    this.parent = tree.Ruleset.prototype;\n    this.frames = frames;\n};\ntree.mixin.Definition.prototype = {\n    type: \"MixinDefinition\",\n    accept: function (visitor) {\n        if (this.params && this.params.length) {\n            this.params = visitor.visitArray(this.params);\n        }\n        this.rules = visitor.visitArray(this.rules);\n        if (this.condition) {\n            this.condition = visitor.visit(this.condition);\n        }\n    },\n    variable:  function (name) { return this.parent.variable.call(this, name); },\n    variables: function ()     { return this.parent.variables.call(this); },\n    find:      function ()     { return this.parent.find.apply(this, arguments); },\n    rulesets:  function ()     { return this.parent.rulesets.apply(this); },\n\n    evalParams: function (env, mixinEnv, args, evaldArguments) {\n        /*jshint boss:true */\n        var frame = new(tree.Ruleset)(null, null),\n            varargs, arg,\n            params = this.params.slice(0),\n            i, j, val, name, isNamedFound, argIndex, argsLength = 0;\n\n        mixinEnv = new tree.evalEnv(mixinEnv, [frame].concat(mixinEnv.frames));\n\n        if (args) {\n            args = args.slice(0);\n            argsLength = args.length;\n\n            for(i = 0; i < argsLength; i++) {\n                arg = args[i];\n                if (name = (arg && arg.name)) {\n                    isNamedFound = false;\n                    for(j = 0; j < params.length; j++) {\n                        if (!evaldArguments[j] && name === params[j].name) {\n                            evaldArguments[j] = arg.value.eval(env);\n                            frame.prependRule(new(tree.Rule)(name, arg.value.eval(env)));\n                            isNamedFound = true;\n                            break;\n                        }\n                    }\n                    if (isNamedFound) {\n                        args.splice(i, 1);\n                        i--;\n                        continue;\n                    } else {\n                        throw { type: 'Runtime', message: \"Named argument for \" + this.name +\n                            ' ' + args[i].name + ' not found' };\n                    }\n                }\n            }\n        }\n        argIndex = 0;\n        for (i = 0; i < params.length; i++) {\n            if (evaldArguments[i]) { continue; }\n\n            arg = args && args[argIndex];\n\n            if (name = params[i].name) {\n                if (params[i].variadic) {\n                    varargs = [];\n                    for (j = argIndex; j < argsLength; j++) {\n                        varargs.push(args[j].value.eval(env));\n                    }\n                    frame.prependRule(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env)));\n                } else {\n                    val = arg && arg.value;\n                    if (val) {\n                        val = val.eval(env);\n                    } else if (params[i].value) {\n                        val = params[i].value.eval(mixinEnv);\n                        frame.resetCache();\n                    } else {\n                        throw { type: 'Runtime', message: \"wrong number of arguments for \" + this.name +\n                            ' (' + argsLength + ' for ' + this.arity + ')' };\n                    }\n                    \n                    frame.prependRule(new(tree.Rule)(name, val));\n                    evaldArguments[i] = val;\n                }\n            }\n\n            if (params[i].variadic && args) {\n                for (j = argIndex; j < argsLength; j++) {\n                    evaldArguments[j] = args[j].value.eval(env);\n                }\n            }\n            argIndex++;\n        }\n\n        return frame;\n    },\n    eval: function (env) {\n        return new tree.mixin.Definition(this.name, this.params, this.rules, this.condition, this.variadic, this.frames || env.frames.slice(0));\n    },\n    evalCall: function (env, args, important) {\n        var _arguments = [],\n            mixinFrames = this.frames ? this.frames.concat(env.frames) : env.frames,\n            frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments),\n            rules, ruleset;\n\n        frame.prependRule(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env)));\n\n        rules = this.rules.slice(0);\n\n        ruleset = new(tree.Ruleset)(null, rules);\n        ruleset.originalRuleset = this;\n        ruleset = ruleset.eval(new(tree.evalEnv)(env, [this, frame].concat(mixinFrames)));\n        if (important) {\n            ruleset = this.parent.makeImportant.apply(ruleset);\n        }\n        return ruleset;\n    },\n    matchCondition: function (args, env) {\n        if (this.condition && !this.condition.eval(\n            new(tree.evalEnv)(env,\n                [this.evalParams(env, new(tree.evalEnv)(env, this.frames.concat(env.frames)), args, [])] // the parameter variables\n                    .concat(this.frames) // the parent namespace/mixin frames\n                    .concat(env.frames)))) { // the current environment frames\n            return false;\n        }\n        return true;\n    },\n    matchArgs: function (args, env) {\n        var argsLength = (args && args.length) || 0, len;\n\n        if (! this.variadic) {\n            if (argsLength < this.required)                               { return false; }\n            if (argsLength > this.params.length)                          { return false; }\n        } else {\n            if (argsLength < (this.required - 1))                         { return false; }\n        }\n\n        len = Math.min(argsLength, this.arity);\n\n        for (var i = 0; i < len; i++) {\n            if (!this.params[i].name && !this.params[i].variadic) {\n                if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Negative = function (node) {\n    this.value = node;\n};\ntree.Negative.prototype = {\n    type: \"Negative\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    genCSS: function (env, output) {\n        output.add('-');\n        this.value.genCSS(env, output);\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        if (env.isMathOn()) {\n            return (new(tree.Operation)('*', [new(tree.Dimension)(-1), this.value])).eval(env);\n        }\n        return new(tree.Negative)(this.value.eval(env));\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Operation = function (op, operands, isSpaced) {\n    this.op = op.trim();\n    this.operands = operands;\n    this.isSpaced = isSpaced;\n};\ntree.Operation.prototype = {\n    type: \"Operation\",\n    accept: function (visitor) {\n        this.operands = visitor.visit(this.operands);\n    },\n    eval: function (env) {\n        var a = this.operands[0].eval(env),\n            b = this.operands[1].eval(env);\n\n        if (env.isMathOn()) {\n            if (a instanceof tree.Dimension && b instanceof tree.Color) {\n                a = a.toColor();\n            }\n            if (b instanceof tree.Dimension && a instanceof tree.Color) {\n                b = b.toColor();\n            }\n            if (!a.operate) {\n                throw { type: \"Operation\",\n                        message: \"Operation on an invalid type\" };\n            }\n\n            return a.operate(env, this.op, b);\n        } else {\n            return new(tree.Operation)(this.op, [a, b], this.isSpaced);\n        }\n    },\n    genCSS: function (env, output) {\n        this.operands[0].genCSS(env, output);\n        if (this.isSpaced) {\n            output.add(\" \");\n        }\n        output.add(this.op);\n        if (this.isSpaced) {\n            output.add(\" \");\n        }\n        this.operands[1].genCSS(env, output);\n    },\n    toCSS: tree.toCSS\n};\n\ntree.operate = function (env, op, a, b) {\n    switch (op) {\n        case '+': return a + b;\n        case '-': return a - b;\n        case '*': return a * b;\n        case '/': return a / b;\n    }\n};\n\n})(require('../tree'));\n\n\n(function (tree) {\n\ntree.Paren = function (node) {\n    this.value = node;\n};\ntree.Paren.prototype = {\n    type: \"Paren\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    genCSS: function (env, output) {\n        output.add('(');\n        this.value.genCSS(env, output);\n        output.add(')');\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        return new(tree.Paren)(this.value.eval(env));\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Quoted = function (str, content, escaped, index, currentFileInfo) {\n    this.escaped = escaped;\n    this.value = content || '';\n    this.quote = str.charAt(0);\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Quoted.prototype = {\n    type: \"Quoted\",\n    genCSS: function (env, output) {\n        if (!this.escaped) {\n            output.add(this.quote, this.currentFileInfo, this.index);\n        }\n        output.add(this.value);\n        if (!this.escaped) {\n            output.add(this.quote);\n        }\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        var that = this;\n        var value = this.value.replace(/`([^`]+)`/g, function (_, exp) {\n            return new(tree.JavaScript)(exp, that.index, true).eval(env).value;\n        }).replace(/@\\{([\\w-]+)\\}/g, function (_, name) {\n            var v = new(tree.Variable)('@' + name, that.index, that.currentFileInfo).eval(env, true);\n            return (v instanceof tree.Quoted) ? v.value : v.toCSS();\n        });\n        return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index, this.currentFileInfo);\n    },\n    compare: function (x) {\n        if (!x.toCSS) {\n            return -1;\n        }\n        \n        var left = this.toCSS(),\n            right = x.toCSS();\n        \n        if (left === right) {\n            return 0;\n        }\n        \n        return left < right ? -1 : 1;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Rule = function (name, value, important, merge, index, currentFileInfo, inline) {\n    this.name = name;\n    this.value = (value instanceof tree.Value || value instanceof tree.Ruleset) ? value : new(tree.Value)([value]);\n    this.important = important ? ' ' + important.trim() : '';\n    this.merge = merge;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n    this.inline = inline || false;\n    this.variable = name.charAt && (name.charAt(0) === '@');\n};\n\ntree.Rule.prototype = {\n    type: \"Rule\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    genCSS: function (env, output) {\n        output.add(this.name + (env.compress ? ':' : ': '), this.currentFileInfo, this.index);\n        try {\n            this.value.genCSS(env, output);\n        }\n        catch(e) {\n            e.index = this.index;\n            e.filename = this.currentFileInfo.filename;\n            throw e;\n        }\n        output.add(this.important + ((this.inline || (env.lastRule && env.compress)) ? \"\" : \";\"), this.currentFileInfo, this.index);\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        var strictMathBypass = false, name = this.name, evaldValue;\n        if (typeof name !== \"string\") {\n            // expand 'primitive' name directly to get\n            // things faster (~10% for benchmark.less):\n            name = (name.length === 1) \n                && (name[0] instanceof tree.Keyword)\n                    ? name[0].value : evalName(env, name);\n        }\n        if (name === \"font\" && !env.strictMath) {\n            strictMathBypass = true;\n            env.strictMath = true;\n        }\n        try {\n            evaldValue = this.value.eval(env);\n            \n            if (!this.variable && evaldValue.type === \"DetachedRuleset\") {\n                throw { message: \"Rulesets cannot be evaluated on a property.\",\n                        index: this.index, filename: this.currentFileInfo.filename };\n            }\n\n            return new(tree.Rule)(name,\n                              evaldValue,\n                              this.important,\n                              this.merge,\n                              this.index, this.currentFileInfo, this.inline);\n        }\n        catch(e) {\n            if (typeof e.index !== 'number') {\n                e.index = this.index;\n                e.filename = this.currentFileInfo.filename;\n            }\n            throw e;\n        }\n        finally {\n            if (strictMathBypass) {\n                env.strictMath = false;\n            }\n        }\n    },\n    makeImportant: function () {\n        return new(tree.Rule)(this.name,\n                              this.value,\n                              \"!important\",\n                              this.merge,\n                              this.index, this.currentFileInfo, this.inline);\n    }\n};\n\nfunction evalName(env, name) {\n    var value = \"\", i, n = name.length,\n        output = {add: function (s) {value += s;}};\n    for (i = 0; i < n; i++) {\n        name[i].eval(env).genCSS(env, output);\n    }\n    return value;\n}\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.RulesetCall = function (variable) {\n    this.variable = variable;\n};\ntree.RulesetCall.prototype = {\n    type: \"RulesetCall\",\n    accept: function (visitor) {\n    },\n    eval: function (env) {\n        var detachedRuleset = new(tree.Variable)(this.variable).eval(env);\n        return detachedRuleset.callEval(env);\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Ruleset = function (selectors, rules, strictImports) {\n    this.selectors = selectors;\n    this.rules = rules;\n    this._lookups = {};\n    this.strictImports = strictImports;\n};\ntree.Ruleset.prototype = {\n    type: \"Ruleset\",\n    accept: function (visitor) {\n        if (this.paths) {\n            visitor.visitArray(this.paths, true);\n        } else if (this.selectors) {\n            this.selectors = visitor.visitArray(this.selectors);\n        }\n        if (this.rules && this.rules.length) {\n            this.rules = visitor.visitArray(this.rules);\n        }\n    },\n    eval: function (env) {\n        var thisSelectors = this.selectors, selectors, \n            selCnt, selector, i, defaultFunc = tree.defaultFunc, hasOnePassingSelector = false;\n\n        if (thisSelectors && (selCnt = thisSelectors.length)) {\n            selectors = [];\n            defaultFunc.error({\n                type: \"Syntax\", \n                message: \"it is currently only allowed in parametric mixin guards,\" \n            });\n            for (i = 0; i < selCnt; i++) {\n                selector = thisSelectors[i].eval(env);\n                selectors.push(selector);\n                if (selector.evaldCondition) {\n                    hasOnePassingSelector = true;\n                }\n            }\n            defaultFunc.reset();  \n        } else {\n            hasOnePassingSelector = true;\n        }\n\n        var rules = this.rules ? this.rules.slice(0) : null,\n            ruleset = new(tree.Ruleset)(selectors, rules, this.strictImports),\n            rule, subRule;\n\n        ruleset.originalRuleset = this;\n        ruleset.root = this.root;\n        ruleset.firstRoot = this.firstRoot;\n        ruleset.allowImports = this.allowImports;\n\n        if(this.debugInfo) {\n            ruleset.debugInfo = this.debugInfo;\n        }\n        \n        if (!hasOnePassingSelector) {\n            rules.length = 0;\n        }\n\n        // push the current ruleset to the frames stack\n        var envFrames = env.frames;\n        envFrames.unshift(ruleset);\n\n        // currrent selectors\n        var envSelectors = env.selectors;\n        if (!envSelectors) {\n            env.selectors = envSelectors = [];\n        }\n        envSelectors.unshift(this.selectors);\n\n        // Evaluate imports\n        if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) {\n            ruleset.evalImports(env);\n        }\n\n        // Store the frames around mixin definitions,\n        // so they can be evaluated like closures when the time comes.\n        var rsRules = ruleset.rules, rsRuleCnt = rsRules ? rsRules.length : 0;\n        for (i = 0; i < rsRuleCnt; i++) {\n            if (rsRules[i] instanceof tree.mixin.Definition || rsRules[i] instanceof tree.DetachedRuleset) {\n                rsRules[i] = rsRules[i].eval(env);\n            }\n        }\n\n        var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0;\n\n        // Evaluate mixin calls.\n        for (i = 0; i < rsRuleCnt; i++) {\n            if (rsRules[i] instanceof tree.mixin.Call) {\n                /*jshint loopfunc:true */\n                rules = rsRules[i].eval(env).filter(function(r) {\n                    if ((r instanceof tree.Rule) && r.variable) {\n                        // do not pollute the scope if the variable is\n                        // already there. consider returning false here\n                        // but we need a way to \"return\" variable from mixins\n                        return !(ruleset.variable(r.name));\n                    }\n                    return true;\n                });\n                rsRules.splice.apply(rsRules, [i, 1].concat(rules));\n                rsRuleCnt += rules.length - 1;\n                i += rules.length-1;\n                ruleset.resetCache();\n            } else if (rsRules[i] instanceof tree.RulesetCall) {\n                /*jshint loopfunc:true */\n                rules = rsRules[i].eval(env).rules.filter(function(r) {\n                    if ((r instanceof tree.Rule) && r.variable) {\n                        // do not pollute the scope at all\n                        return false;\n                    }\n                    return true;\n                });\n                rsRules.splice.apply(rsRules, [i, 1].concat(rules));\n                rsRuleCnt += rules.length - 1;\n                i += rules.length-1;\n                ruleset.resetCache();\n            }\n        }\n\n        // Evaluate everything else\n        for (i = 0; i < rsRules.length; i++) {\n            rule = rsRules[i];\n            if (! (rule instanceof tree.mixin.Definition || rule instanceof tree.DetachedRuleset)) {\n                rsRules[i] = rule = rule.eval ? rule.eval(env) : rule;\n            }\n        }\n        \n        // Evaluate everything else\n        for (i = 0; i < rsRules.length; i++) {\n            rule = rsRules[i];\n            // for rulesets, check if it is a css guard and can be removed\n            if (rule instanceof tree.Ruleset && rule.selectors && rule.selectors.length === 1) {\n                // check if it can be folded in (e.g. & where)\n                if (rule.selectors[0].isJustParentSelector()) {\n                    rsRules.splice(i--, 1);\n\n                    for(var j = 0; j < rule.rules.length; j++) {\n                        subRule = rule.rules[j];\n                        if (!(subRule instanceof tree.Rule) || !subRule.variable) {\n                            rsRules.splice(++i, 0, subRule);\n                        }\n                    }\n                }\n            }\n        }\n\n        // Pop the stack\n        envFrames.shift();\n        envSelectors.shift();\n        \n        if (env.mediaBlocks) {\n            for (i = mediaBlockCount; i < env.mediaBlocks.length; i++) {\n                env.mediaBlocks[i].bubbleSelectors(selectors);\n            }\n        }\n\n        return ruleset;\n    },\n    evalImports: function(env) {\n        var rules = this.rules, i, importRules;\n        if (!rules) { return; }\n\n        for (i = 0; i < rules.length; i++) {\n            if (rules[i] instanceof tree.Import) {\n                importRules = rules[i].eval(env);\n                if (importRules && importRules.length) {\n                    rules.splice.apply(rules, [i, 1].concat(importRules));\n                    i+= importRules.length-1;\n                } else {\n                    rules.splice(i, 1, importRules);\n                }\n                this.resetCache();\n            }\n        }\n    },\n    makeImportant: function() {\n        return new tree.Ruleset(this.selectors, this.rules.map(function (r) {\n                    if (r.makeImportant) {\n                        return r.makeImportant();\n                    } else {\n                        return r;\n                    }\n                }), this.strictImports);\n    },\n    matchArgs: function (args) {\n        return !args || args.length === 0;\n    },\n    // lets you call a css selector with a guard\n    matchCondition: function (args, env) {\n        var lastSelector = this.selectors[this.selectors.length-1];\n        if (!lastSelector.evaldCondition) {\n            return false;\n        }\n        if (lastSelector.condition &&\n            !lastSelector.condition.eval(\n                new(tree.evalEnv)(env,\n                    env.frames))) {\n            return false;\n        }\n        return true;\n    },\n    resetCache: function () {\n        this._rulesets = null;\n        this._variables = null;\n        this._lookups = {};\n    },\n    variables: function () {\n        if (!this._variables) {\n            this._variables = !this.rules ? {} : this.rules.reduce(function (hash, r) {\n                if (r instanceof tree.Rule && r.variable === true) {\n                    hash[r.name] = r;\n                }\n                return hash;\n            }, {});\n        }\n        return this._variables;\n    },\n    variable: function (name) {\n        return this.variables()[name];\n    },\n    rulesets: function () {\n        if (!this.rules) { return null; }\n\n        var _Ruleset = tree.Ruleset, _MixinDefinition = tree.mixin.Definition,\n            filtRules = [], rules = this.rules, cnt = rules.length,\n            i, rule;\n\n        for (i = 0; i < cnt; i++) {\n            rule = rules[i];\n            if ((rule instanceof _Ruleset) || (rule instanceof _MixinDefinition)) {\n                filtRules.push(rule);\n            }\n        }\n\n        return filtRules;\n    },\n    prependRule: function (rule) {\n        var rules = this.rules;\n        if (rules) { rules.unshift(rule); } else { this.rules = [ rule ]; }\n    },\n    find: function (selector, self) {\n        self = self || this;\n        var rules = [], match,\n            key = selector.toCSS();\n\n        if (key in this._lookups) { return this._lookups[key]; }\n\n        this.rulesets().forEach(function (rule) {\n            if (rule !== self) {\n                for (var j = 0; j < rule.selectors.length; j++) {\n                    match = selector.match(rule.selectors[j]);\n                    if (match) {\n                        if (selector.elements.length > match) {\n                            Array.prototype.push.apply(rules, rule.find(\n                                new(tree.Selector)(selector.elements.slice(match)), self));\n                        } else {\n                            rules.push(rule);\n                        }\n                        break;\n                    }\n                }\n            }\n        });\n        this._lookups[key] = rules;\n        return rules;\n    },\n    genCSS: function (env, output) {\n        var i, j,\n            ruleNodes = [],\n            rulesetNodes = [],\n            rulesetNodeCnt,\n            debugInfo,     // Line number debugging\n            rule,\n            path;\n\n        env.tabLevel = (env.tabLevel || 0);\n\n        if (!this.root) {\n            env.tabLevel++;\n        }\n\n        var tabRuleStr = env.compress ? '' : Array(env.tabLevel + 1).join(\"  \"),\n            tabSetStr = env.compress ? '' : Array(env.tabLevel).join(\"  \"),\n            sep;\n\n        for (i = 0; i < this.rules.length; i++) {\n            rule = this.rules[i];\n            if (rule.rules || (rule instanceof tree.Media) || rule instanceof tree.Directive || (this.root && rule instanceof tree.Comment)) {\n                rulesetNodes.push(rule);\n            } else {\n                ruleNodes.push(rule);\n            }\n        }\n\n        // If this is the root node, we don't render\n        // a selector, or {}.\n        if (!this.root) {\n            debugInfo = tree.debugInfo(env, this, tabSetStr);\n\n            if (debugInfo) {\n                output.add(debugInfo);\n                output.add(tabSetStr);\n            }\n\n            var paths = this.paths, pathCnt = paths.length,\n                pathSubCnt;\n\n            sep = env.compress ? ',' : (',\\n' + tabSetStr);\n\n            for (i = 0; i < pathCnt; i++) {\n                path = paths[i];\n                if (!(pathSubCnt = path.length)) { continue; }\n                if (i > 0) { output.add(sep); }\n\n                env.firstSelector = true;\n                path[0].genCSS(env, output);\n\n                env.firstSelector = false;\n                for (j = 1; j < pathSubCnt; j++) {\n                    path[j].genCSS(env, output);\n                }\n            }\n\n            output.add((env.compress ? '{' : ' {\\n') + tabRuleStr);\n        }\n\n        // Compile rules and rulesets\n        for (i = 0; i < ruleNodes.length; i++) {\n            rule = ruleNodes[i];\n\n            // @page{ directive ends up with root elements inside it, a mix of rules and rulesets\n            // In this instance we do not know whether it is the last property\n            if (i + 1 === ruleNodes.length && (!this.root || rulesetNodes.length === 0 || this.firstRoot)) {\n                env.lastRule = true;\n            }\n\n            if (rule.genCSS) {\n                rule.genCSS(env, output);\n            } else if (rule.value) {\n                output.add(rule.value.toString());\n            }\n\n            if (!env.lastRule) {\n                output.add(env.compress ? '' : ('\\n' + tabRuleStr));\n            } else {\n                env.lastRule = false;\n            }\n        }\n\n        if (!this.root) {\n            output.add((env.compress ? '}' : '\\n' + tabSetStr + '}'));\n            env.tabLevel--;\n        }\n\n        sep = (env.compress ? \"\" : \"\\n\") + (this.root ? tabRuleStr : tabSetStr);\n        rulesetNodeCnt = rulesetNodes.length;\n        if (rulesetNodeCnt) {\n            if (ruleNodes.length && sep) { output.add(sep); }\n            rulesetNodes[0].genCSS(env, output);\n            for (i = 1; i < rulesetNodeCnt; i++) {\n                if (sep) { output.add(sep); }\n                rulesetNodes[i].genCSS(env, output);\n            }\n        }\n\n        if (!output.isEmpty() && !env.compress && this.firstRoot) {\n            output.add('\\n');\n        }\n    },\n\n    toCSS: tree.toCSS,\n\n    markReferenced: function () {\n        if (!this.selectors) {\n            return;\n        }\n        for (var s = 0; s < this.selectors.length; s++) {\n            this.selectors[s].markReferenced();\n        }\n    },\n\n    joinSelectors: function (paths, context, selectors) {\n        for (var s = 0; s < selectors.length; s++) {\n            this.joinSelector(paths, context, selectors[s]);\n        }\n    },\n\n    joinSelector: function (paths, context, selector) {\n\n        var i, j, k, \n            hasParentSelector, newSelectors, el, sel, parentSel, \n            newSelectorPath, afterParentJoin, newJoinedSelector, \n            newJoinedSelectorEmpty, lastSelector, currentElements,\n            selectorsMultiplied;\n    \n        for (i = 0; i < selector.elements.length; i++) {\n            el = selector.elements[i];\n            if (el.value === '&') {\n                hasParentSelector = true;\n            }\n        }\n    \n        if (!hasParentSelector) {\n            if (context.length > 0) {\n                for (i = 0; i < context.length; i++) {\n                    paths.push(context[i].concat(selector));\n                }\n            }\n            else {\n                paths.push([selector]);\n            }\n            return;\n        }\n\n        // The paths are [[Selector]]\n        // The first list is a list of comma seperated selectors\n        // The inner list is a list of inheritance seperated selectors\n        // e.g.\n        // .a, .b {\n        //   .c {\n        //   }\n        // }\n        // == [[.a] [.c]] [[.b] [.c]]\n        //\n\n        // the elements from the current selector so far\n        currentElements = [];\n        // the current list of new selectors to add to the path.\n        // We will build it up. We initiate it with one empty selector as we \"multiply\" the new selectors\n        // by the parents\n        newSelectors = [[]];\n\n        for (i = 0; i < selector.elements.length; i++) {\n            el = selector.elements[i];\n            // non parent reference elements just get added\n            if (el.value !== \"&\") {\n                currentElements.push(el);\n            } else {\n                // the new list of selectors to add\n                selectorsMultiplied = [];\n\n                // merge the current list of non parent selector elements\n                // on to the current list of selectors to add\n                if (currentElements.length > 0) {\n                    this.mergeElementsOnToSelectors(currentElements, newSelectors);\n                }\n\n                // loop through our current selectors\n                for (j = 0; j < newSelectors.length; j++) {\n                    sel = newSelectors[j];\n                    // if we don't have any parent paths, the & might be in a mixin so that it can be used\n                    // whether there are parents or not\n                    if (context.length === 0) {\n                        // the combinator used on el should now be applied to the next element instead so that\n                        // it is not lost\n                        if (sel.length > 0) {\n                            sel[0].elements = sel[0].elements.slice(0);\n                            sel[0].elements.push(new(tree.Element)(el.combinator, '', el.index, el.currentFileInfo));\n                        }\n                        selectorsMultiplied.push(sel);\n                    }\n                    else {\n                        // and the parent selectors\n                        for (k = 0; k < context.length; k++) {\n                            parentSel = context[k];\n                            // We need to put the current selectors\n                            // then join the last selector's elements on to the parents selectors\n\n                            // our new selector path\n                            newSelectorPath = [];\n                            // selectors from the parent after the join\n                            afterParentJoin = [];\n                            newJoinedSelectorEmpty = true;\n\n                            //construct the joined selector - if & is the first thing this will be empty,\n                            // if not newJoinedSelector will be the last set of elements in the selector\n                            if (sel.length > 0) {\n                                newSelectorPath = sel.slice(0);\n                                lastSelector = newSelectorPath.pop();\n                                newJoinedSelector = selector.createDerived(lastSelector.elements.slice(0));\n                                newJoinedSelectorEmpty = false;\n                            }\n                            else {\n                                newJoinedSelector = selector.createDerived([]);\n                            }\n\n                            //put together the parent selectors after the join\n                            if (parentSel.length > 1) {\n                                afterParentJoin = afterParentJoin.concat(parentSel.slice(1));\n                            }\n\n                            if (parentSel.length > 0) {\n                                newJoinedSelectorEmpty = false;\n\n                                // join the elements so far with the first part of the parent\n                                newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, el.index, el.currentFileInfo));\n                                newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1));\n                            }\n\n                            if (!newJoinedSelectorEmpty) {\n                                // now add the joined selector\n                                newSelectorPath.push(newJoinedSelector);\n                            }\n\n                            // and the rest of the parent\n                            newSelectorPath = newSelectorPath.concat(afterParentJoin);\n\n                            // add that to our new set of selectors\n                            selectorsMultiplied.push(newSelectorPath);\n                        }\n                    }\n                }\n\n                // our new selectors has been multiplied, so reset the state\n                newSelectors = selectorsMultiplied;\n                currentElements = [];\n            }\n        }\n\n        // if we have any elements left over (e.g. .a& .b == .b)\n        // add them on to all the current selectors\n        if (currentElements.length > 0) {\n            this.mergeElementsOnToSelectors(currentElements, newSelectors);\n        }\n\n        for (i = 0; i < newSelectors.length; i++) {\n            if (newSelectors[i].length > 0) {\n                paths.push(newSelectors[i]);\n            }\n        }\n    },\n    \n    mergeElementsOnToSelectors: function(elements, selectors) {\n        var i, sel;\n\n        if (selectors.length === 0) {\n            selectors.push([ new(tree.Selector)(elements) ]);\n            return;\n        }\n\n        for (i = 0; i < selectors.length; i++) {\n            sel = selectors[i];\n\n            // if the previous thing in sel is a parent this needs to join on to it\n            if (sel.length > 0) {\n                sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements));\n            }\n            else {\n                sel.push(new(tree.Selector)(elements));\n            }\n        }\n    }\n};\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Selector = function (elements, extendList, condition, index, currentFileInfo, isReferenced) {\n    this.elements = elements;\n    this.extendList = extendList;\n    this.condition = condition;\n    this.currentFileInfo = currentFileInfo || {};\n    this.isReferenced = isReferenced;\n    if (!condition) {\n        this.evaldCondition = true;\n    }\n};\ntree.Selector.prototype = {\n    type: \"Selector\",\n    accept: function (visitor) {\n        if (this.elements) {\n            this.elements = visitor.visitArray(this.elements);\n        }\n        if (this.extendList) {\n            this.extendList = visitor.visitArray(this.extendList);\n        }\n        if (this.condition) {\n            this.condition = visitor.visit(this.condition);\n        }\n    },\n    createDerived: function(elements, extendList, evaldCondition) {\n        evaldCondition = (evaldCondition != null) ? evaldCondition : this.evaldCondition;\n        var newSelector = new(tree.Selector)(elements, extendList || this.extendList, null, this.index, this.currentFileInfo, this.isReferenced);\n        newSelector.evaldCondition = evaldCondition;\n        newSelector.mediaEmpty = this.mediaEmpty;\n        return newSelector;\n    },\n    match: function (other) {\n        var elements = this.elements,\n            len = elements.length,\n            olen, i;\n\n        other.CacheElements();\n\n        olen = other._elements.length;\n        if (olen === 0 || len < olen) {\n            return 0;\n        } else {\n            for (i = 0; i < olen; i++) {\n                if (elements[i].value !== other._elements[i]) {\n                    return 0;\n                }\n            }\n        }\n\n        return olen; // return number of matched elements\n    },\n    CacheElements: function(){\n        var css = '', len, v, i;\n\n        if( !this._elements ){\n\n            len = this.elements.length;\n            for(i = 0; i < len; i++){\n\n                v = this.elements[i];\n                css += v.combinator.value;\n\n                if( !v.value.value ){\n                    css += v.value;\n                    continue;\n                }\n\n                if( typeof v.value.value !== \"string\" ){\n                    css = '';\n                    break;\n                }\n                css += v.value.value;\n            }\n\n            this._elements = css.match(/[,&#\\.\\w-]([\\w-]|(\\\\.))*/g);\n\n            if (this._elements) {\n                if (this._elements[0] === \"&\") {\n                    this._elements.shift();\n                }\n\n            } else {\n                this._elements = [];\n            }\n\n        }\n    },\n    isJustParentSelector: function() {\n        return !this.mediaEmpty && \n            this.elements.length === 1 && \n            this.elements[0].value === '&' && \n            (this.elements[0].combinator.value === ' ' || this.elements[0].combinator.value === '');\n    },\n    eval: function (env) {\n        var evaldCondition = this.condition && this.condition.eval(env),\n            elements = this.elements, extendList = this.extendList;\n\n        elements = elements && elements.map(function (e) { return e.eval(env); });\n        extendList = extendList && extendList.map(function(extend) { return extend.eval(env); });\n\n        return this.createDerived(elements, extendList, evaldCondition);\n    },\n    genCSS: function (env, output) {\n        var i, element;\n        if ((!env || !env.firstSelector) && this.elements[0].combinator.value === \"\") {\n            output.add(' ', this.currentFileInfo, this.index);\n        }\n        if (!this._css) {\n            //TODO caching? speed comparison?\n            for(i = 0; i < this.elements.length; i++) {\n                element = this.elements[i];\n                element.genCSS(env, output);\n            }\n        }\n    },\n    toCSS: tree.toCSS,\n    markReferenced: function () {\n        this.isReferenced = true;\n    },\n    getIsReferenced: function() {\n        return !this.currentFileInfo.reference || this.isReferenced;\n    },\n    getIsOutput: function() {\n        return this.evaldCondition;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.UnicodeDescriptor = function (value) {\n    this.value = value;\n};\ntree.UnicodeDescriptor.prototype = {\n    type: \"UnicodeDescriptor\",\n    genCSS: function (env, output) {\n        output.add(this.value);\n    },\n    toCSS: tree.toCSS,\n    eval: function () { return this; }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.URL = function (val, currentFileInfo, isEvald) {\n    this.value = val;\n    this.currentFileInfo = currentFileInfo;\n    this.isEvald = isEvald;\n};\ntree.URL.prototype = {\n    type: \"Url\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    genCSS: function (env, output) {\n        output.add(\"url(\");\n        this.value.genCSS(env, output);\n        output.add(\")\");\n    },\n    toCSS: tree.toCSS,\n    eval: function (ctx) {\n        var val = this.value.eval(ctx),\n            rootpath;\n\n        if (!this.isEvald) {\n            // Add the base path if the URL is relative\n            rootpath = this.currentFileInfo && this.currentFileInfo.rootpath;\n            if (rootpath && typeof val.value === \"string\" && ctx.isPathRelative(val.value)) {\n                if (!val.quote) {\n                    rootpath = rootpath.replace(/[\\(\\)'\"\\s]/g, function(match) { return \"\\\\\"+match; });\n                }\n                val.value = rootpath + val.value;\n            }\n            \n            val.value = ctx.normalizePath(val.value);\n\n            // Add url args if enabled\n            if (ctx.urlArgs) {\n                if (!val.value.match(/^\\s*data:/)) {\n                    var delimiter = val.value.indexOf('?') === -1 ? '?' : '&';\n                    var urlArgs = delimiter + ctx.urlArgs;\n                    if (val.value.indexOf('#') !== -1) {\n                        val.value = val.value.replace('#', urlArgs + '#');\n                    } else {\n                        val.value += urlArgs;\n                    }\n                }\n            }\n        }\n\n        return new(tree.URL)(val, this.currentFileInfo, true);\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Value = function (value) {\n    this.value = value;\n};\ntree.Value.prototype = {\n    type: \"Value\",\n    accept: function (visitor) {\n        if (this.value) {\n            this.value = visitor.visitArray(this.value);\n        }\n    },\n    eval: function (env) {\n        if (this.value.length === 1) {\n            return this.value[0].eval(env);\n        } else {\n            return new(tree.Value)(this.value.map(function (v) {\n                return v.eval(env);\n            }));\n        }\n    },\n    genCSS: function (env, output) {\n        var i;\n        for(i = 0; i < this.value.length; i++) {\n            this.value[i].genCSS(env, output);\n            if (i+1 < this.value.length) {\n                output.add((env && env.compress) ? ',' : ', ');\n            }\n        }\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Variable = function (name, index, currentFileInfo) {\n    this.name = name;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo || {};\n};\ntree.Variable.prototype = {\n    type: \"Variable\",\n    eval: function (env) {\n        var variable, name = this.name;\n\n        if (name.indexOf('@@') === 0) {\n            name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value;\n        }\n        \n        if (this.evaluating) {\n            throw { type: 'Name',\n                    message: \"Recursive variable definition for \" + name,\n                    filename: this.currentFileInfo.file,\n                    index: this.index };\n        }\n        \n        this.evaluating = true;\n\n        variable = tree.find(env.frames, function (frame) {\n            var v = frame.variable(name);\n            if (v) {\n                return v.value.eval(env);\n            }\n        });\n        if (variable) { \n            this.evaluating = false;\n            return variable;\n        } else {\n            throw { type: 'Name',\n                    message: \"variable \" + name + \" is undefined\",\n                    filename: this.currentFileInfo.filename,\n                    index: this.index };\n        }\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\n    var parseCopyProperties = [\n        'paths',            // option - unmodified - paths to search for imports on\n        'optimization',     // option - optimization level (for the chunker)\n        'files',            // list of files that have been imported, used for import-once\n        'contents',         // map - filename to contents of all the files\n        'contentsIgnoredChars', // map - filename to lines at the begining of each file to ignore\n        'relativeUrls',     // option - whether to adjust URL's to be relative\n        'rootpath',         // option - rootpath to append to URL's\n        'strictImports',    // option -\n        'insecure',         // option - whether to allow imports from insecure ssl hosts\n        'dumpLineNumbers',  // option - whether to dump line numbers\n        'compress',         // option - whether to compress\n        'processImports',   // option - whether to process imports. if false then imports will not be imported\n        'syncImport',       // option - whether to import synchronously\n        'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true\n        'mime',             // browser only - mime type for sheet import\n        'useFileCache',     // browser only - whether to use the per file session cache\n        'currentFileInfo'   // information about the current file - for error reporting and importing and making urls relative etc.\n    ];\n\n    //currentFileInfo = {\n    //  'relativeUrls' - option - whether to adjust URL's to be relative\n    //  'filename' - full resolved filename of current file\n    //  'rootpath' - path to append to normal URLs for this node\n    //  'currentDirectory' - path to the current file, absolute\n    //  'rootFilename' - filename of the base file\n    //  'entryPath' - absolute path to the entry file\n    //  'reference' - whether the file should not be output and only output parts that are referenced\n\n    tree.parseEnv = function(options) {\n        copyFromOriginal(options, this, parseCopyProperties);\n\n        if (!this.contents) { this.contents = {}; }\n        if (!this.contentsIgnoredChars) { this.contentsIgnoredChars = {}; }\n        if (!this.files) { this.files = {}; }\n\n        if (!this.currentFileInfo) {\n            var filename = (options && options.filename) || \"input\";\n            var entryPath = filename.replace(/[^\\/\\\\]*$/, \"\");\n            if (options) {\n                options.filename = null;\n            }\n            this.currentFileInfo = {\n                filename: filename,\n                relativeUrls: this.relativeUrls,\n                rootpath: (options && options.rootpath) || \"\",\n                currentDirectory: entryPath,\n                entryPath: entryPath,\n                rootFilename: filename\n            };\n        }\n    };\n\n    var evalCopyProperties = [\n        'silent',         // whether to swallow errors and warnings\n        'verbose',        // whether to log more activity\n        'compress',       // whether to compress\n        'yuicompress',    // whether to compress with the outside tool yui compressor\n        'ieCompat',       // whether to enforce IE compatibility (IE8 data-uri)\n        'strictMath',     // whether math has to be within parenthesis\n        'strictUnits',    // whether units need to evaluate correctly\n        'cleancss',       // whether to compress with clean-css\n        'sourceMap',      // whether to output a source map\n        'importMultiple', // whether we are currently importing multiple copies\n        'urlArgs'         // whether to add args into url tokens\n        ];\n\n    tree.evalEnv = function(options, frames) {\n        copyFromOriginal(options, this, evalCopyProperties);\n\n        this.frames = frames || [];\n    };\n\n    tree.evalEnv.prototype.inParenthesis = function () {\n        if (!this.parensStack) {\n            this.parensStack = [];\n        }\n        this.parensStack.push(true);\n    };\n\n    tree.evalEnv.prototype.outOfParenthesis = function () {\n        this.parensStack.pop();\n    };\n\n    tree.evalEnv.prototype.isMathOn = function () {\n        return this.strictMath ? (this.parensStack && this.parensStack.length) : true;\n    };\n\n    tree.evalEnv.prototype.isPathRelative = function (path) {\n        return !/^(?:[a-z-]+:|\\/)/.test(path);\n    };\n\n    tree.evalEnv.prototype.normalizePath = function( path ) {\n        var\n          segments = path.split(\"/\").reverse(),\n          segment;\n\n        path = [];\n        while (segments.length !== 0 ) {\n            segment = segments.pop();\n            switch( segment ) {\n                case \".\":\n                    break;\n                case \"..\":\n                    if ((path.length === 0) || (path[path.length - 1] === \"..\")) {\n                        path.push( segment );\n                    } else {\n                        path.pop();\n                    }\n                    break;\n                default:\n                    path.push( segment );\n                    break;\n            }\n        }\n\n        return path.join(\"/\");\n    };\n\n    //todo - do the same for the toCSS env\n    //tree.toCSSEnv = function (options) {\n    //};\n\n    var copyFromOriginal = function(original, destination, propertiesToCopy) {\n        if (!original) { return; }\n\n        for(var i = 0; i < propertiesToCopy.length; i++) {\n            if (original.hasOwnProperty(propertiesToCopy[i])) {\n                destination[propertiesToCopy[i]] = original[propertiesToCopy[i]];\n            }\n        }\n    };\n\n})(require('./tree'));\n\n(function (tree) {\n\n    var _visitArgs = { visitDeeper: true },\n        _hasIndexed = false;\n\n    function _noop(node) {\n        return node;\n    }\n\n    function indexNodeTypes(parent, ticker) {\n        // add .typeIndex to tree node types for lookup table\n        var key, child;\n        for (key in parent) {\n            if (parent.hasOwnProperty(key)) {\n                child = parent[key];\n                switch (typeof child) {\n                    case \"function\":\n                        // ignore bound functions directly on tree which do not have a prototype\n                        // or aren't nodes\n                        if (child.prototype && child.prototype.type) {\n                            child.prototype.typeIndex = ticker++;\n                        }\n                        break;\n                    case \"object\":\n                        ticker = indexNodeTypes(child, ticker);\n                        break;\n                }\n            }\n        }\n        return ticker;\n    }\n\n    tree.visitor = function(implementation) {\n        this._implementation = implementation;\n        this._visitFnCache = [];\n\n        if (!_hasIndexed) {\n            indexNodeTypes(tree, 1);\n            _hasIndexed = true;\n        }\n    };\n\n    tree.visitor.prototype = {\n        visit: function(node) {\n            if (!node) {\n                return node;\n            }\n\n            var nodeTypeIndex = node.typeIndex;\n            if (!nodeTypeIndex) {\n                return node;\n            }\n\n            var visitFnCache = this._visitFnCache,\n                impl = this._implementation,\n                aryIndx = nodeTypeIndex << 1,\n                outAryIndex = aryIndx | 1,\n                func = visitFnCache[aryIndx],\n                funcOut = visitFnCache[outAryIndex],\n                visitArgs = _visitArgs,\n                fnName;\n\n            visitArgs.visitDeeper = true;\n\n            if (!func) {\n                fnName = \"visit\" + node.type;\n                func = impl[fnName] || _noop;\n                funcOut = impl[fnName + \"Out\"] || _noop;\n                visitFnCache[aryIndx] = func;\n                visitFnCache[outAryIndex] = funcOut;\n            }\n\n            if (func !== _noop) {\n                var newNode = func.call(impl, node, visitArgs);\n                if (impl.isReplacing) {\n                    node = newNode;\n                }\n            }\n\n            if (visitArgs.visitDeeper && node && node.accept) {\n                node.accept(this);\n            }\n\n            if (funcOut != _noop) {\n                funcOut.call(impl, node);\n            }\n\n            return node;\n        },\n        visitArray: function(nodes, nonReplacing) {\n            if (!nodes) {\n                return nodes;\n            }\n\n            var cnt = nodes.length, i;\n\n            // Non-replacing\n            if (nonReplacing || !this._implementation.isReplacing) {\n                for (i = 0; i < cnt; i++) {\n                    this.visit(nodes[i]);\n                }\n                return nodes;\n            }\n\n            // Replacing\n            var out = [];\n            for (i = 0; i < cnt; i++) {\n                var evald = this.visit(nodes[i]);\n                if (!evald.splice) {\n                    out.push(evald);\n                } else if (evald.length) {\n                    this.flatten(evald, out);\n                }\n            }\n            return out;\n        },\n        flatten: function(arr, out) {\n            if (!out) {\n                out = [];\n            }\n\n            var cnt, i, item,\n                nestedCnt, j, nestedItem;\n\n            for (i = 0, cnt = arr.length; i < cnt; i++) {\n                item = arr[i];\n                if (!item.splice) {\n                    out.push(item);\n                    continue;\n                }\n\n                for (j = 0, nestedCnt = item.length; j < nestedCnt; j++) {\n                    nestedItem = item[j];\n                    if (!nestedItem.splice) {\n                        out.push(nestedItem);\n                    } else if (nestedItem.length) {\n                        this.flatten(nestedItem, out);\n                    }\n                }\n            }\n\n            return out;\n        }\n    };\n\n})(require('./tree'));\n(function (tree) {\n    tree.importVisitor = function(importer, finish, evalEnv, onceFileDetectionMap, recursionDetector) {\n        this._visitor = new tree.visitor(this);\n        this._importer = importer;\n        this._finish = finish;\n        this.env = evalEnv || new tree.evalEnv();\n        this.importCount = 0;\n        this.onceFileDetectionMap = onceFileDetectionMap || {};\n        this.recursionDetector = {};\n        if (recursionDetector) {\n            for(var fullFilename in recursionDetector) {\n                if (recursionDetector.hasOwnProperty(fullFilename)) {\n                    this.recursionDetector[fullFilename] = true;\n                }\n            }\n        }\n    };\n\n    tree.importVisitor.prototype = {\n        isReplacing: true,\n        run: function (root) {\n            var error;\n            try {\n                // process the contents\n                this._visitor.visit(root);\n            }\n            catch(e) {\n                error = e;\n            }\n\n            this.isFinished = true;\n\n            if (this.importCount === 0) {\n                this._finish(error);\n            }\n        },\n        visitImport: function (importNode, visitArgs) {\n            var importVisitor = this,\n                evaldImportNode,\n                inlineCSS = importNode.options.inline;\n            \n            if (!importNode.css || inlineCSS) {\n\n                try {\n                    evaldImportNode = importNode.evalForImport(this.env);\n                } catch(e){\n                    if (!e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; }\n                    // attempt to eval properly and treat as css\n                    importNode.css = true;\n                    // if that fails, this error will be thrown\n                    importNode.error = e;\n                }\n\n                if (evaldImportNode && (!evaldImportNode.css || inlineCSS)) {\n                    importNode = evaldImportNode;\n                    this.importCount++;\n                    var env = new tree.evalEnv(this.env, this.env.frames.slice(0));\n\n                    if (importNode.options.multiple) {\n                        env.importMultiple = true;\n                    }\n\n                    this._importer.push(importNode.getPath(), importNode.currentFileInfo, importNode.options, function (e, root, importedAtRoot, fullPath) {\n                        if (e && !e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; }\n\n                        if (!env.importMultiple) { \n                            if (importedAtRoot) {\n                                importNode.skip = true;\n                            } else {\n                                importNode.skip = function() {\n                                    if (fullPath in importVisitor.onceFileDetectionMap) {\n                                        return true;\n                                    }\n                                    importVisitor.onceFileDetectionMap[fullPath] = true;\n                                    return false;\n                                }; \n                            }\n                        }\n\n                        var subFinish = function(e) {\n                            importVisitor.importCount--;\n\n                            if (importVisitor.importCount === 0 && importVisitor.isFinished) {\n                                importVisitor._finish(e);\n                            }\n                        };\n\n                        if (root) {\n                            importNode.root = root;\n                            importNode.importedFilename = fullPath;\n                            var duplicateImport = importedAtRoot || fullPath in importVisitor.recursionDetector;\n\n                            if (!inlineCSS && (env.importMultiple || !duplicateImport)) {\n                                importVisitor.recursionDetector[fullPath] = true;\n                                new(tree.importVisitor)(importVisitor._importer, subFinish, env, importVisitor.onceFileDetectionMap, importVisitor.recursionDetector)\n                                    .run(root);\n                                return;\n                            }\n                        }\n\n                        subFinish();\n                    });\n                }\n            }\n            visitArgs.visitDeeper = false;\n            return importNode;\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n            return ruleNode;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            this.env.frames.unshift(directiveNode);\n            return directiveNode;\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.env.frames.shift();\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            this.env.frames.unshift(mixinDefinitionNode);\n            return mixinDefinitionNode;\n        },\n        visitMixinDefinitionOut: function (mixinDefinitionNode) {\n            this.env.frames.shift();\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n            this.env.frames.unshift(rulesetNode);\n            return rulesetNode;\n        },\n        visitRulesetOut: function (rulesetNode) {\n            this.env.frames.shift();\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            this.env.frames.unshift(mediaNode.ruleset);\n            return mediaNode;\n        },\n        visitMediaOut: function (mediaNode) {\n            this.env.frames.shift();\n        }\n    };\n\n})(require('./tree'));\n(function (tree) {\n    tree.joinSelectorVisitor = function() {\n        this.contexts = [[]];\n        this._visitor = new tree.visitor(this);\n    };\n\n    tree.joinSelectorVisitor.prototype = {\n        run: function (root) {\n            return this._visitor.visit(root);\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n\n        visitRuleset: function (rulesetNode, visitArgs) {\n            var context = this.contexts[this.contexts.length - 1],\n                paths = [], selectors;\n\n            this.contexts.push(paths);\n\n            if (! rulesetNode.root) {\n                selectors = rulesetNode.selectors;\n                if (selectors) {\n                    selectors = selectors.filter(function(selector) { return selector.getIsOutput(); });\n                    rulesetNode.selectors = selectors.length ? selectors : (selectors = null);\n                    if (selectors) { rulesetNode.joinSelectors(paths, context, selectors); }\n                }\n                if (!selectors) { rulesetNode.rules = null; }\n                rulesetNode.paths = paths;\n            }\n        },\n        visitRulesetOut: function (rulesetNode) {\n            this.contexts.length = this.contexts.length - 1;\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            var context = this.contexts[this.contexts.length - 1];\n            mediaNode.rules[0].root = (context.length === 0 || context[0].multiMedia);\n        }\n    };\n\n})(require('./tree'));\n(function (tree) {\n    tree.toCSSVisitor = function(env) {\n        this._visitor = new tree.visitor(this);\n        this._env = env;\n    };\n\n    tree.toCSSVisitor.prototype = {\n        isReplacing: true,\n        run: function (root) {\n            return this._visitor.visit(root);\n        },\n\n        visitRule: function (ruleNode, visitArgs) {\n            if (ruleNode.variable) {\n                return [];\n            }\n            return ruleNode;\n        },\n\n        visitMixinDefinition: function (mixinNode, visitArgs) {\n            // mixin definitions do not get eval'd - this means they keep state\n            // so we have to clear that state here so it isn't used if toCSS is called twice\n            mixinNode.frames = [];\n            return [];\n        },\n\n        visitExtend: function (extendNode, visitArgs) {\n            return [];\n        },\n\n        visitComment: function (commentNode, visitArgs) {\n            if (commentNode.isSilent(this._env)) {\n                return [];\n            }\n            return commentNode;\n        },\n\n        visitMedia: function(mediaNode, visitArgs) {\n            mediaNode.accept(this._visitor);\n            visitArgs.visitDeeper = false;\n\n            if (!mediaNode.rules.length) {\n                return [];\n            }\n            return mediaNode;\n        },\n\n        visitDirective: function(directiveNode, visitArgs) {\n            if (directiveNode.currentFileInfo.reference && !directiveNode.isReferenced) {\n                return [];\n            }\n            if (directiveNode.name === \"@charset\") {\n                // Only output the debug info together with subsequent @charset definitions\n                // a comment (or @media statement) before the actual @charset directive would\n                // be considered illegal css as it has to be on the first line\n                if (this.charset) {\n                    if (directiveNode.debugInfo) {\n                        var comment = new tree.Comment(\"/* \" + directiveNode.toCSS(this._env).replace(/\\n/g, \"\")+\" */\\n\");\n                        comment.debugInfo = directiveNode.debugInfo;\n                        return this._visitor.visit(comment);\n                    }\n                    return [];\n                }\n                this.charset = true;\n            }\n            return directiveNode;\n        },\n\n        checkPropertiesInRoot: function(rules) {\n            var ruleNode;\n            for(var i = 0; i < rules.length; i++) {\n                ruleNode = rules[i];\n                if (ruleNode instanceof tree.Rule && !ruleNode.variable) {\n                    throw { message: \"properties must be inside selector blocks, they cannot be in the root.\",\n                        index: ruleNode.index, filename: ruleNode.currentFileInfo ? ruleNode.currentFileInfo.filename : null};\n                }\n            }\n        },\n\n        visitRuleset: function (rulesetNode, visitArgs) {\n            var rule, rulesets = [];\n            if (rulesetNode.firstRoot) {\n                this.checkPropertiesInRoot(rulesetNode.rules);\n            }\n            if (! rulesetNode.root) {\n                if (rulesetNode.paths) {\n                    rulesetNode.paths = rulesetNode.paths\n                        .filter(function(p) {\n                            var i;\n                            if (p[0].elements[0].combinator.value === ' ') {\n                                p[0].elements[0].combinator = new(tree.Combinator)('');\n                            }\n                            for(i = 0; i < p.length; i++) {\n                                if (p[i].getIsReferenced() && p[i].getIsOutput()) {\n                                    return true;\n                                }\n                            }\n                            return false;\n                        });\n                }\n\n                // Compile rules and rulesets\n                var nodeRules = rulesetNode.rules, nodeRuleCnt = nodeRules ? nodeRules.length : 0;\n                for (var i = 0; i < nodeRuleCnt; ) {\n                    rule = nodeRules[i];\n                    if (rule && rule.rules) {\n                        // visit because we are moving them out from being a child\n                        rulesets.push(this._visitor.visit(rule));\n                        nodeRules.splice(i, 1);\n                        nodeRuleCnt--;\n                        continue;\n                    }\n                    i++;\n                }\n                // accept the visitor to remove rules and refactor itself\n                // then we can decide now whether we want it or not\n                if (nodeRuleCnt > 0) {\n                    rulesetNode.accept(this._visitor);\n                } else {\n                    rulesetNode.rules = null;\n                }\n                visitArgs.visitDeeper = false;\n\n                nodeRules = rulesetNode.rules;\n                if (nodeRules) {\n                    this._mergeRules(nodeRules);\n                    nodeRules = rulesetNode.rules;\n                }\n                if (nodeRules) {\n                    this._removeDuplicateRules(nodeRules);\n                    nodeRules = rulesetNode.rules;\n                }\n\n                // now decide whether we keep the ruleset\n                if (nodeRules && nodeRules.length > 0 && rulesetNode.paths.length > 0) {\n                    rulesets.splice(0, 0, rulesetNode);\n                }\n            } else {\n                rulesetNode.accept(this._visitor);\n                visitArgs.visitDeeper = false;\n                if (rulesetNode.firstRoot || (rulesetNode.rules && rulesetNode.rules.length > 0)) {\n                    rulesets.splice(0, 0, rulesetNode);\n                }\n            }\n            if (rulesets.length === 1) {\n                return rulesets[0];\n            }\n            return rulesets;\n        },\n\n        _removeDuplicateRules: function(rules) {\n            if (!rules) { return; }\n\n            // remove duplicates\n            var ruleCache = {},\n                ruleList, rule, i;\n\n            for(i = rules.length - 1; i >= 0 ; i--) {\n                rule = rules[i];\n                if (rule instanceof tree.Rule) {\n                    if (!ruleCache[rule.name]) {\n                        ruleCache[rule.name] = rule;\n                    } else {\n                        ruleList = ruleCache[rule.name];\n                        if (ruleList instanceof tree.Rule) {\n                            ruleList = ruleCache[rule.name] = [ruleCache[rule.name].toCSS(this._env)];\n                        }\n                        var ruleCSS = rule.toCSS(this._env);\n                        if (ruleList.indexOf(ruleCSS) !== -1) {\n                            rules.splice(i, 1);\n                        } else {\n                            ruleList.push(ruleCSS);\n                        }\n                    }\n                }\n            }\n        },\n\n        _mergeRules: function (rules) {\n            if (!rules) { return; }\n\n            var groups = {},\n                parts,\n                rule,\n                key;\n\n            for (var i = 0; i < rules.length; i++) {\n                rule = rules[i];\n\n                if ((rule instanceof tree.Rule) && rule.merge) {\n                    key = [rule.name,\n                        rule.important ? \"!\" : \"\"].join(\",\");\n\n                    if (!groups[key]) {\n                        groups[key] = [];\n                    } else {\n                        rules.splice(i--, 1);\n                    }\n\n                    groups[key].push(rule);\n                }\n            }\n\n            Object.keys(groups).map(function (k) {\n\n                function toExpression(values) {\n                    return new (tree.Expression)(values.map(function (p) {\n                        return p.value;\n                    }));\n                }\n\n                function toValue(values) {\n                    return new (tree.Value)(values.map(function (p) {\n                        return p;\n                    }));\n                }\n\n                parts = groups[k];\n\n                if (parts.length > 1) {\n                    rule = parts[0];\n                    var spacedGroups = [];\n                    var lastSpacedGroup = [];\n                    parts.map(function (p) {\n                    if (p.merge===\"+\") {\n                        if (lastSpacedGroup.length > 0) {\n                                spacedGroups.push(toExpression(lastSpacedGroup));\n                            }\n                            lastSpacedGroup = [];\n                        }\n                        lastSpacedGroup.push(p);\n                    });\n                    spacedGroups.push(toExpression(lastSpacedGroup));\n                    rule.value = toValue(spacedGroups);\n                }\n            });\n        }\n    };\n\n})(require('./tree'));\n(function (tree) {\n    /*jshint loopfunc:true */\n\n    tree.extendFinderVisitor = function() {\n        this._visitor = new tree.visitor(this);\n        this.contexts = [];\n        this.allExtendsStack = [[]];\n    };\n\n    tree.extendFinderVisitor.prototype = {\n        run: function (root) {\n            root = this._visitor.visit(root);\n            root.allExtends = this.allExtendsStack[0];\n            return root;\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n            if (rulesetNode.root) {\n                return;\n            }\n\n            var i, j, extend, allSelectorsExtendList = [], extendList;\n\n            // get &:extend(.a); rules which apply to all selectors in this ruleset\n            var rules = rulesetNode.rules, ruleCnt = rules ? rules.length : 0;\n            for(i = 0; i < ruleCnt; i++) {\n                if (rulesetNode.rules[i] instanceof tree.Extend) {\n                    allSelectorsExtendList.push(rules[i]);\n                    rulesetNode.extendOnEveryPath = true;\n                }\n            }\n\n            // now find every selector and apply the extends that apply to all extends\n            // and the ones which apply to an individual extend\n            var paths = rulesetNode.paths;\n            for(i = 0; i < paths.length; i++) {\n                var selectorPath = paths[i],\n                    selector = selectorPath[selectorPath.length - 1],\n                    selExtendList = selector.extendList;\n\n                extendList = selExtendList ? selExtendList.slice(0).concat(allSelectorsExtendList)\n                                           : allSelectorsExtendList;\n\n                if (extendList) {\n                    extendList = extendList.map(function(allSelectorsExtend) {\n                        return allSelectorsExtend.clone();\n                    });\n                }\n\n                for(j = 0; j < extendList.length; j++) {\n                    this.foundExtends = true;\n                    extend = extendList[j];\n                    extend.findSelfSelectors(selectorPath);\n                    extend.ruleset = rulesetNode;\n                    if (j === 0) { extend.firstExtendOnThisSelectorPath = true; }\n                    this.allExtendsStack[this.allExtendsStack.length-1].push(extend);\n                }\n            }\n\n            this.contexts.push(rulesetNode.selectors);\n        },\n        visitRulesetOut: function (rulesetNode) {\n            if (!rulesetNode.root) {\n                this.contexts.length = this.contexts.length - 1;\n            }\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            mediaNode.allExtends = [];\n            this.allExtendsStack.push(mediaNode.allExtends);\n        },\n        visitMediaOut: function (mediaNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            directiveNode.allExtends = [];\n            this.allExtendsStack.push(directiveNode.allExtends);\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        }\n    };\n\n    tree.processExtendsVisitor = function() {\n        this._visitor = new tree.visitor(this);\n    };\n\n    tree.processExtendsVisitor.prototype = {\n        run: function(root) {\n            var extendFinder = new tree.extendFinderVisitor();\n            extendFinder.run(root);\n            if (!extendFinder.foundExtends) { return root; }\n            root.allExtends = root.allExtends.concat(this.doExtendChaining(root.allExtends, root.allExtends));\n            this.allExtendsStack = [root.allExtends];\n            return this._visitor.visit(root);\n        },\n        doExtendChaining: function (extendsList, extendsListTarget, iterationCount) {\n            //\n            // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting\n            // the selector we would do normally, but we are also adding an extend with the same target selector\n            // this means this new extend can then go and alter other extends\n            //\n            // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors\n            // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if\n            // we look at each selector at a time, as is done in visitRuleset\n\n            var extendIndex, targetExtendIndex, matches, extendsToAdd = [], newSelector, extendVisitor = this, selectorPath, extend, targetExtend, newExtend;\n\n            iterationCount = iterationCount || 0;\n\n            //loop through comparing every extend with every target extend.\n            // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place\n            // e.g.  .a:extend(.b) {}  and .b:extend(.c) {} then the first extend extends the second one\n            // and the second is the target.\n            // the seperation into two lists allows us to process a subset of chains with a bigger set, as is the\n            // case when processing media queries\n            for(extendIndex = 0; extendIndex < extendsList.length; extendIndex++){\n                for(targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++){\n\n                    extend = extendsList[extendIndex];\n                    targetExtend = extendsListTarget[targetExtendIndex];\n\n                    // look for circular references\n                    if( extend.parent_ids.indexOf( targetExtend.object_id ) >= 0 ){ continue; }\n\n                    // find a match in the target extends self selector (the bit before :extend)\n                    selectorPath = [targetExtend.selfSelectors[0]];\n                    matches = extendVisitor.findMatch(extend, selectorPath);\n\n                    if (matches.length) {\n\n                        // we found a match, so for each self selector..\n                        extend.selfSelectors.forEach(function(selfSelector) {\n\n                            // process the extend as usual\n                            newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector);\n\n                            // but now we create a new extend from it\n                            newExtend = new(tree.Extend)(targetExtend.selector, targetExtend.option, 0);\n                            newExtend.selfSelectors = newSelector;\n\n                            // add the extend onto the list of extends for that selector\n                            newSelector[newSelector.length-1].extendList = [newExtend];\n\n                            // record that we need to add it.\n                            extendsToAdd.push(newExtend);\n                            newExtend.ruleset = targetExtend.ruleset;\n\n                            //remember its parents for circular references\n                            newExtend.parent_ids = newExtend.parent_ids.concat(targetExtend.parent_ids, extend.parent_ids);\n\n                            // only process the selector once.. if we have :extend(.a,.b) then multiple\n                            // extends will look at the same selector path, so when extending\n                            // we know that any others will be duplicates in terms of what is added to the css\n                            if (targetExtend.firstExtendOnThisSelectorPath) {\n                                newExtend.firstExtendOnThisSelectorPath = true;\n                                targetExtend.ruleset.paths.push(newSelector);\n                            }\n                        });\n                    }\n                }\n            }\n\n            if (extendsToAdd.length) {\n                // try to detect circular references to stop a stack overflow.\n                // may no longer be needed.\n                this.extendChainCount++;\n                if (iterationCount > 100) {\n                    var selectorOne = \"{unable to calculate}\";\n                    var selectorTwo = \"{unable to calculate}\";\n                    try\n                    {\n                        selectorOne = extendsToAdd[0].selfSelectors[0].toCSS();\n                        selectorTwo = extendsToAdd[0].selector.toCSS();\n                    }\n                    catch(e) {}\n                    throw {message: \"extend circular reference detected. One of the circular extends is currently:\"+selectorOne+\":extend(\" + selectorTwo+\")\"};\n                }\n\n                // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e...\n                return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount+1));\n            } else {\n                return extendsToAdd;\n            }\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitSelector: function (selectorNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n            if (rulesetNode.root) {\n                return;\n            }\n            var matches, pathIndex, extendIndex, allExtends = this.allExtendsStack[this.allExtendsStack.length-1], selectorsToAdd = [], extendVisitor = this, selectorPath;\n\n            // look at each selector path in the ruleset, find any extend matches and then copy, find and replace\n\n            for(extendIndex = 0; extendIndex < allExtends.length; extendIndex++) {\n                for(pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) {\n                    selectorPath = rulesetNode.paths[pathIndex];\n\n                    // extending extends happens initially, before the main pass\n                    if (rulesetNode.extendOnEveryPath) { continue; }\n                    var extendList = selectorPath[selectorPath.length-1].extendList;\n                    if (extendList && extendList.length) { continue; }\n\n                    matches = this.findMatch(allExtends[extendIndex], selectorPath);\n\n                    if (matches.length) {\n\n                        allExtends[extendIndex].selfSelectors.forEach(function(selfSelector) {\n                            selectorsToAdd.push(extendVisitor.extendSelector(matches, selectorPath, selfSelector));\n                        });\n                    }\n                }\n            }\n            rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd);\n        },\n        findMatch: function (extend, haystackSelectorPath) {\n            //\n            // look through the haystack selector path to try and find the needle - extend.selector\n            // returns an array of selector matches that can then be replaced\n            //\n            var haystackSelectorIndex, hackstackSelector, hackstackElementIndex, haystackElement,\n                targetCombinator, i,\n                extendVisitor = this,\n                needleElements = extend.selector.elements,\n                potentialMatches = [], potentialMatch, matches = [];\n\n            // loop through the haystack elements\n            for(haystackSelectorIndex = 0; haystackSelectorIndex < haystackSelectorPath.length; haystackSelectorIndex++) {\n                hackstackSelector = haystackSelectorPath[haystackSelectorIndex];\n\n                for(hackstackElementIndex = 0; hackstackElementIndex < hackstackSelector.elements.length; hackstackElementIndex++) {\n\n                    haystackElement = hackstackSelector.elements[hackstackElementIndex];\n\n                    // if we allow elements before our match we can add a potential match every time. otherwise only at the first element.\n                    if (extend.allowBefore || (haystackSelectorIndex === 0 && hackstackElementIndex === 0)) {\n                        potentialMatches.push({pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0, initialCombinator: haystackElement.combinator});\n                    }\n\n                    for(i = 0; i < potentialMatches.length; i++) {\n                        potentialMatch = potentialMatches[i];\n\n                        // selectors add \" \" onto the first element. When we use & it joins the selectors together, but if we don't\n                        // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out\n                        // what the resulting combinator will be\n                        targetCombinator = haystackElement.combinator.value;\n                        if (targetCombinator === '' && hackstackElementIndex === 0) {\n                            targetCombinator = ' ';\n                        }\n\n                        // if we don't match, null our match to indicate failure\n                        if (!extendVisitor.isElementValuesEqual(needleElements[potentialMatch.matched].value, haystackElement.value) ||\n                            (potentialMatch.matched > 0 && needleElements[potentialMatch.matched].combinator.value !== targetCombinator)) {\n                            potentialMatch = null;\n                        } else {\n                            potentialMatch.matched++;\n                        }\n\n                        // if we are still valid and have finished, test whether we have elements after and whether these are allowed\n                        if (potentialMatch) {\n                            potentialMatch.finished = potentialMatch.matched === needleElements.length;\n                            if (potentialMatch.finished &&\n                                (!extend.allowAfter && (hackstackElementIndex+1 < hackstackSelector.elements.length || haystackSelectorIndex+1 < haystackSelectorPath.length))) {\n                                potentialMatch = null;\n                            }\n                        }\n                        // if null we remove, if not, we are still valid, so either push as a valid match or continue\n                        if (potentialMatch) {\n                            if (potentialMatch.finished) {\n                                potentialMatch.length = needleElements.length;\n                                potentialMatch.endPathIndex = haystackSelectorIndex;\n                                potentialMatch.endPathElementIndex = hackstackElementIndex + 1; // index after end of match\n                                potentialMatches.length = 0; // we don't allow matches to overlap, so start matching again\n                                matches.push(potentialMatch);\n                            }\n                        } else {\n                            potentialMatches.splice(i, 1);\n                            i--;\n                        }\n                    }\n                }\n            }\n            return matches;\n        },\n        isElementValuesEqual: function(elementValue1, elementValue2) {\n            if (typeof elementValue1 === \"string\" || typeof elementValue2 === \"string\") {\n                return elementValue1 === elementValue2;\n            }\n            if (elementValue1 instanceof tree.Attribute) {\n                if (elementValue1.op !== elementValue2.op || elementValue1.key !== elementValue2.key) {\n                    return false;\n                }\n                if (!elementValue1.value || !elementValue2.value) {\n                    if (elementValue1.value || elementValue2.value) {\n                        return false;\n                    }\n                    return true;\n                }\n                elementValue1 = elementValue1.value.value || elementValue1.value;\n                elementValue2 = elementValue2.value.value || elementValue2.value;\n                return elementValue1 === elementValue2;\n            }\n            elementValue1 = elementValue1.value;\n            elementValue2 = elementValue2.value;\n            if (elementValue1 instanceof tree.Selector) {\n                if (!(elementValue2 instanceof tree.Selector) || elementValue1.elements.length !== elementValue2.elements.length) {\n                    return false;\n                }\n                for(var i = 0; i <elementValue1.elements.length; i++) {\n                    if (elementValue1.elements[i].combinator.value !== elementValue2.elements[i].combinator.value) {\n                        if (i !== 0 || (elementValue1.elements[i].combinator.value || ' ') !== (elementValue2.elements[i].combinator.value || ' ')) {\n                            return false;\n                        }\n                    }\n                    if (!this.isElementValuesEqual(elementValue1.elements[i].value, elementValue2.elements[i].value)) {\n                        return false;\n                    }\n                }\n                return true;\n            }\n            return false;\n        },\n        extendSelector:function (matches, selectorPath, replacementSelector) {\n\n            //for a set of matches, replace each match with the replacement selector\n\n            var currentSelectorPathIndex = 0,\n                currentSelectorPathElementIndex = 0,\n                path = [],\n                matchIndex,\n                selector,\n                firstElement,\n                match,\n                newElements;\n\n            for (matchIndex = 0; matchIndex < matches.length; matchIndex++) {\n                match = matches[matchIndex];\n                selector = selectorPath[match.pathIndex];\n                firstElement = new tree.Element(\n                    match.initialCombinator,\n                    replacementSelector.elements[0].value,\n                    replacementSelector.elements[0].index,\n                    replacementSelector.elements[0].currentFileInfo\n                );\n\n                if (match.pathIndex > currentSelectorPathIndex && currentSelectorPathElementIndex > 0) {\n                    path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));\n                    currentSelectorPathElementIndex = 0;\n                    currentSelectorPathIndex++;\n                }\n\n                newElements = selector.elements\n                    .slice(currentSelectorPathElementIndex, match.index)\n                    .concat([firstElement])\n                    .concat(replacementSelector.elements.slice(1));\n\n                if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) {\n                    path[path.length - 1].elements =\n                        path[path.length - 1].elements.concat(newElements);\n                } else {\n                    path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex));\n\n                    path.push(new tree.Selector(\n                        newElements\n                    ));\n                }\n                currentSelectorPathIndex = match.endPathIndex;\n                currentSelectorPathElementIndex = match.endPathElementIndex;\n                if (currentSelectorPathElementIndex >= selectorPath[currentSelectorPathIndex].elements.length) {\n                    currentSelectorPathElementIndex = 0;\n                    currentSelectorPathIndex++;\n                }\n            }\n\n            if (currentSelectorPathIndex < selectorPath.length && currentSelectorPathElementIndex > 0) {\n                path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));\n                currentSelectorPathIndex++;\n            }\n\n            path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length));\n\n            return path;\n        },\n        visitRulesetOut: function (rulesetNode) {\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);\n            newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends));\n            this.allExtendsStack.push(newAllExtends);\n        },\n        visitMediaOut: function (mediaNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            var newAllExtends = directiveNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);\n            newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, directiveNode.allExtends));\n            this.allExtendsStack.push(newAllExtends);\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        }\n    };\n\n})(require('./tree'));\n\n(function (tree) {\n\n    tree.sourceMapOutput = function (options) {\n        this._css = [];\n        this._rootNode = options.rootNode;\n        this._writeSourceMap = options.writeSourceMap;\n        this._contentsMap = options.contentsMap;\n        this._contentsIgnoredCharsMap = options.contentsIgnoredCharsMap;\n        this._sourceMapFilename = options.sourceMapFilename;\n        this._outputFilename = options.outputFilename;\n        this._sourceMapURL = options.sourceMapURL;\n        if (options.sourceMapBasepath) {\n            this._sourceMapBasepath = options.sourceMapBasepath.replace(/\\\\/g, '/');\n        }\n        this._sourceMapRootpath = options.sourceMapRootpath;\n        this._outputSourceFiles = options.outputSourceFiles;\n        this._sourceMapGeneratorConstructor = options.sourceMapGenerator || require(\"source-map\").SourceMapGenerator;\n\n        if (this._sourceMapRootpath && this._sourceMapRootpath.charAt(this._sourceMapRootpath.length-1) !== '/') {\n            this._sourceMapRootpath += '/';\n        }\n\n        this._lineNumber = 0;\n        this._column = 0;\n    };\n\n    tree.sourceMapOutput.prototype.normalizeFilename = function(filename) {\n        filename = filename.replace(/\\\\/g, '/');\n\n        if (this._sourceMapBasepath && filename.indexOf(this._sourceMapBasepath) === 0) {\n            filename = filename.substring(this._sourceMapBasepath.length);\n            if (filename.charAt(0) === '\\\\' || filename.charAt(0) === '/') {\n               filename = filename.substring(1);\n            }\n        }\n        return (this._sourceMapRootpath || \"\") + filename;\n    };\n\n    tree.sourceMapOutput.prototype.add = function(chunk, fileInfo, index, mapLines) {\n\n        //ignore adding empty strings\n        if (!chunk) {\n            return;\n        }\n\n        var lines,\n            sourceLines,\n            columns,\n            sourceColumns,\n            i;\n\n        if (fileInfo) {\n            var inputSource = this._contentsMap[fileInfo.filename];\n            \n            // remove vars/banner added to the top of the file\n            if (this._contentsIgnoredCharsMap[fileInfo.filename]) {\n                // adjust the index\n                index -= this._contentsIgnoredCharsMap[fileInfo.filename];\n                if (index < 0) { index = 0; }\n                // adjust the source\n                inputSource = inputSource.slice(this._contentsIgnoredCharsMap[fileInfo.filename]);\n            }\n            inputSource = inputSource.substring(0, index);\n            sourceLines = inputSource.split(\"\\n\");\n            sourceColumns = sourceLines[sourceLines.length-1];\n        }\n\n        lines = chunk.split(\"\\n\");\n        columns = lines[lines.length-1];\n\n        if (fileInfo) {\n            if (!mapLines) {\n                this._sourceMapGenerator.addMapping({ generated: { line: this._lineNumber + 1, column: this._column},\n                    original: { line: sourceLines.length, column: sourceColumns.length},\n                    source: this.normalizeFilename(fileInfo.filename)});\n            } else {\n                for(i = 0; i < lines.length; i++) {\n                    this._sourceMapGenerator.addMapping({ generated: { line: this._lineNumber + i + 1, column: i === 0 ? this._column : 0},\n                        original: { line: sourceLines.length + i, column: i === 0 ? sourceColumns.length : 0},\n                        source: this.normalizeFilename(fileInfo.filename)});\n                }\n            }\n        }\n\n        if (lines.length === 1) {\n            this._column += columns.length;\n        } else {\n            this._lineNumber += lines.length - 1;\n            this._column = columns.length;\n        }\n\n        this._css.push(chunk);\n    };\n\n    tree.sourceMapOutput.prototype.isEmpty = function() {\n        return this._css.length === 0;\n    };\n\n    tree.sourceMapOutput.prototype.toCSS = function(env) {\n        this._sourceMapGenerator = new this._sourceMapGeneratorConstructor({ file: this._outputFilename, sourceRoot: null });\n\n        if (this._outputSourceFiles) {\n            for(var filename in this._contentsMap) {\n                if (this._contentsMap.hasOwnProperty(filename))\n                {\n                    var source = this._contentsMap[filename];\n                    if (this._contentsIgnoredCharsMap[filename]) {\n                        source = source.slice(this._contentsIgnoredCharsMap[filename]);\n                    }\n                    this._sourceMapGenerator.setSourceContent(this.normalizeFilename(filename), source);\n                }\n            }\n        }\n\n        this._rootNode.genCSS(env, this);\n\n        if (this._css.length > 0) {\n            var sourceMapURL,\n                sourceMapContent = JSON.stringify(this._sourceMapGenerator.toJSON());\n\n            if (this._sourceMapURL) {\n                sourceMapURL = this._sourceMapURL;\n            } else if (this._sourceMapFilename) {\n                sourceMapURL = this.normalizeFilename(this._sourceMapFilename);\n            }\n\n            if (this._writeSourceMap) {\n                this._writeSourceMap(sourceMapContent);\n            } else {\n                sourceMapURL = \"data:application/json,\" + encodeURIComponent(sourceMapContent);\n            }\n\n            if (sourceMapURL) {\n                this._css.push(\"/*# sourceMappingURL=\" + sourceMapURL + \" */\");\n            }\n        }\n\n        return this._css.join('');\n    };\n\n})(require('./tree'));\n\n//\n// browser.js - client-side engine\n//\n/*global less, window, document, XMLHttpRequest, location */\n\nvar isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol);\n\nless.env = less.env || (location.hostname == '127.0.0.1' ||\n                        location.hostname == '0.0.0.0'   ||\n                        location.hostname == 'localhost' ||\n                        (location.port &&\n                          location.port.length > 0)      ||\n                        isFileProtocol                   ? 'development'\n                                                         : 'production');\n\nvar logLevel = {\n    debug: 3,\n    info: 2,\n    errors: 1,\n    none: 0\n};\n\n// The amount of logging in the javascript console.\n// 3 - Debug, information and errors\n// 2 - Information and errors\n// 1 - Errors\n// 0 - None\n// Defaults to 2\nless.logLevel = typeof(less.logLevel) != 'undefined' ? less.logLevel : (less.env === 'development' ?  logLevel.debug : logLevel.errors);\n\n// Load styles asynchronously (default: false)\n//\n// This is set to `false` by default, so that the body\n// doesn't start loading before the stylesheets are parsed.\n// Setting this to `true` can result in flickering.\n//\nless.async = less.async || false;\nless.fileAsync = less.fileAsync || false;\n\n// Interval between watch polls\nless.poll = less.poll || (isFileProtocol ? 1000 : 1500);\n\n//Setup user functions\nif (less.functions) {\n    for(var func in less.functions) {\n        if (less.functions.hasOwnProperty(func)) {\n            less.tree.functions[func] = less.functions[func];\n        }\n   }\n}\n\nvar dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash);\nif (dumpLineNumbers) {\n    less.dumpLineNumbers = dumpLineNumbers[1];\n}\n\nvar typePattern = /^text\\/(x-)?less$/;\nvar cache = null;\nvar fileCache = {};\n\nfunction log(str, level) {\n    if (typeof(console) !== 'undefined' && less.logLevel >= level) {\n        console.log('less: ' + str);\n    }\n}\n\nfunction extractId(href) {\n    return href.replace(/^[a-z-]+:\\/+?[^\\/]+/, '' )  // Remove protocol & domain\n        .replace(/^\\//,                 '' )  // Remove root /\n        .replace(/\\.[a-zA-Z]+$/,        '' )  // Remove simple extension\n        .replace(/[^\\.\\w-]+/g,          '-')  // Replace illegal characters\n        .replace(/\\./g,                 ':'); // Replace dots with colons(for valid id)\n}\n\nfunction errorConsole(e, rootHref) {\n    var template = '{line} {content}';\n    var filename = e.filename || rootHref;\n    var errors = [];\n    var content = (e.type || \"Syntax\") + \"Error: \" + (e.message || 'There is an error in your .less file') +\n        \" in \" + filename + \" \";\n\n    var errorline = function (e, i, classname) {\n        if (e.extract[i] !== undefined) {\n            errors.push(template.replace(/\\{line\\}/, (parseInt(e.line, 10) || 0) + (i - 1))\n                .replace(/\\{class\\}/, classname)\n                .replace(/\\{content\\}/, e.extract[i]));\n        }\n    };\n\n    if (e.extract) {\n        errorline(e, 0, '');\n        errorline(e, 1, 'line');\n        errorline(e, 2, '');\n        content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\\n' +\n            errors.join('\\n');\n    } else if (e.stack) {\n        content += e.stack;\n    }\n    log(content, logLevel.errors);\n}\n\nfunction createCSS(styles, sheet, lastModified) {\n    // Strip the query-string\n    var href = sheet.href || '';\n\n    // If there is no title set, use the filename, minus the extension\n    var id = 'less:' + (sheet.title || extractId(href));\n\n    // If this has already been inserted into the DOM, we may need to replace it\n    var oldCss = document.getElementById(id);\n    var keepOldCss = false;\n\n    // Create a new stylesheet node for insertion or (if necessary) replacement\n    var css = document.createElement('style');\n    css.setAttribute('type', 'text/css');\n    if (sheet.media) {\n        css.setAttribute('media', sheet.media);\n    }\n    css.id = id;\n\n    if (css.styleSheet) { // IE\n        try {\n            css.styleSheet.cssText = styles;\n        } catch (e) {\n            throw new(Error)(\"Couldn't reassign styleSheet.cssText.\");\n        }\n    } else {\n        css.appendChild(document.createTextNode(styles));\n\n        // If new contents match contents of oldCss, don't replace oldCss\n        keepOldCss = (oldCss !== null && oldCss.childNodes.length > 0 && css.childNodes.length > 0 &&\n            oldCss.firstChild.nodeValue === css.firstChild.nodeValue);\n    }\n\n    var head = document.getElementsByTagName('head')[0];\n\n    // If there is no oldCss, just append; otherwise, only append if we need\n    // to replace oldCss with an updated stylesheet\n    if (oldCss === null || keepOldCss === false) {\n        var nextEl = sheet && sheet.nextSibling || null;\n        if (nextEl) {\n            nextEl.parentNode.insertBefore(css, nextEl);\n        } else {\n            head.appendChild(css);\n        }\n    }\n    if (oldCss && keepOldCss === false) {\n        oldCss.parentNode.removeChild(oldCss);\n    }\n\n    // Don't update the local store if the file wasn't modified\n    if (lastModified && cache) {\n        log('saving ' + href + ' to cache.', logLevel.info);\n        try {\n            cache.setItem(href, styles);\n            cache.setItem(href + ':timestamp', lastModified);\n        } catch(e) {\n            //TODO - could do with adding more robust error handling\n            log('failed to save', logLevel.errors);\n        }\n    }\n}\n\nfunction postProcessCSS(styles) {\n    if (less.postProcessor && typeof less.postProcessor === 'function') {\n        styles = less.postProcessor.call(styles, styles) || styles;\n    }\n    return styles;\n}\n\nfunction errorHTML(e, rootHref) {\n    var id = 'less-error-message:' + extractId(rootHref || \"\");\n    var template = '<li><label>{line}</label><pre class=\"{class}\">{content}</pre></li>';\n    var elem = document.createElement('div'), timer, content, errors = [];\n    var filename = e.filename || rootHref;\n    var filenameNoPath = filename.match(/([^\\/]+(\\?.*)?)$/)[1];\n\n    elem.id        = id;\n    elem.className = \"less-error-message\";\n\n    content = '<h3>'  + (e.type || \"Syntax\") + \"Error: \" + (e.message || 'There is an error in your .less file') +\n        '</h3>' + '<p>in <a href=\"' + filename   + '\">' + filenameNoPath + \"</a> \";\n\n    var errorline = function (e, i, classname) {\n        if (e.extract[i] !== undefined) {\n            errors.push(template.replace(/\\{line\\}/, (parseInt(e.line, 10) || 0) + (i - 1))\n                .replace(/\\{class\\}/, classname)\n                .replace(/\\{content\\}/, e.extract[i]));\n        }\n    };\n\n    if (e.extract) {\n        errorline(e, 0, '');\n        errorline(e, 1, 'line');\n        errorline(e, 2, '');\n        content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':</p>' +\n            '<ul>' + errors.join('') + '</ul>';\n    } else if (e.stack) {\n        content += '<br/>' + e.stack.split('\\n').slice(1).join('<br/>');\n    }\n    elem.innerHTML = content;\n\n    // CSS for error messages\n    createCSS([\n        '.less-error-message ul, .less-error-message li {',\n        'list-style-type: none;',\n        'margin-right: 15px;',\n        'padding: 4px 0;',\n        'margin: 0;',\n        '}',\n        '.less-error-message label {',\n        'font-size: 12px;',\n        'margin-right: 15px;',\n        'padding: 4px 0;',\n        'color: #cc7777;',\n        '}',\n        '.less-error-message pre {',\n        'color: #dd6666;',\n        'padding: 4px 0;',\n        'margin: 0;',\n        'display: inline-block;',\n        '}',\n        '.less-error-message pre.line {',\n        'color: #ff0000;',\n        '}',\n        '.less-error-message h3 {',\n        'font-size: 20px;',\n        'font-weight: bold;',\n        'padding: 15px 0 5px 0;',\n        'margin: 0;',\n        '}',\n        '.less-error-message a {',\n        'color: #10a',\n        '}',\n        '.less-error-message .error {',\n        'color: red;',\n        'font-weight: bold;',\n        'padding-bottom: 2px;',\n        'border-bottom: 1px dashed red;',\n        '}'\n    ].join('\\n'), { title: 'error-message' });\n\n    elem.style.cssText = [\n        \"font-family: Arial, sans-serif\",\n        \"border: 1px solid #e00\",\n        \"background-color: #eee\",\n        \"border-radius: 5px\",\n        \"-webkit-border-radius: 5px\",\n        \"-moz-border-radius: 5px\",\n        \"color: #e00\",\n        \"padding: 15px\",\n        \"margin-bottom: 15px\"\n    ].join(';');\n\n    if (less.env == 'development') {\n        timer = setInterval(function () {\n            if (document.body) {\n                if (document.getElementById(id)) {\n                    document.body.replaceChild(elem, document.getElementById(id));\n                } else {\n                    document.body.insertBefore(elem, document.body.firstChild);\n                }\n                clearInterval(timer);\n            }\n        }, 10);\n    }\n}\n\nfunction error(e, rootHref) {\n    if (!less.errorReporting || less.errorReporting === \"html\") {\n        errorHTML(e, rootHref);\n    } else if (less.errorReporting === \"console\") {\n        errorConsole(e, rootHref);\n    } else if (typeof less.errorReporting === 'function') {\n        less.errorReporting(\"add\", e, rootHref);\n    }\n}\n\nfunction removeErrorHTML(path) {\n    var node = document.getElementById('less-error-message:' + extractId(path));\n    if (node) {\n        node.parentNode.removeChild(node);\n    }\n}\n\nfunction removeErrorConsole(path) {\n    //no action\n}\n\nfunction removeError(path) {\n    if (!less.errorReporting || less.errorReporting === \"html\") {\n        removeErrorHTML(path);\n    } else if (less.errorReporting === \"console\") {\n        removeErrorConsole(path);\n    } else if (typeof less.errorReporting === 'function') {\n        less.errorReporting(\"remove\", path);\n    }\n}\n\nfunction loadStyles(modifyVars) {\n    var styles = document.getElementsByTagName('style'),\n        style;\n    for (var i = 0; i < styles.length; i++) {\n        style = styles[i];\n        if (style.type.match(typePattern)) {\n            var env = new less.tree.parseEnv(less),\n                lessText = style.innerHTML || '';\n            env.filename = document.location.href.replace(/#.*$/, '');\n\n            if (modifyVars || less.globalVars) {\n                env.useFileCache = true;\n            }\n\n            /*jshint loopfunc:true */\n            // use closure to store current value of i\n            var callback = (function(style) {\n                return function (e, cssAST) {\n                    if (e) {\n                        return error(e, \"inline\");\n                    }\n                    var css = cssAST.toCSS(less);\n                    style.type = 'text/css';\n                    if (style.styleSheet) {\n                        style.styleSheet.cssText = css;\n                    } else {\n                        style.innerHTML = css;\n                    }\n                };\n            })(style);\n            new(less.Parser)(env).parse(lessText, callback, {globalVars: less.globalVars, modifyVars: modifyVars});\n        }\n    }\n}\n\nfunction extractUrlParts(url, baseUrl) {\n    // urlParts[1] = protocol&hostname || /\n    // urlParts[2] = / if path relative to host base\n    // urlParts[3] = directories\n    // urlParts[4] = filename\n    // urlParts[5] = parameters\n\n    var urlPartsRegex = /^((?:[a-z-]+:)?\\/+?(?:[^\\/\\?#]*\\/)|([\\/\\\\]))?((?:[^\\/\\\\\\?#]*[\\/\\\\])*)([^\\/\\\\\\?#]*)([#\\?].*)?$/i,\n        urlParts = url.match(urlPartsRegex),\n        returner = {}, directories = [], i, baseUrlParts;\n\n    if (!urlParts) {\n        throw new Error(\"Could not parse sheet href - '\"+url+\"'\");\n    }\n\n    // Stylesheets in IE don't always return the full path\n    if (!urlParts[1] || urlParts[2]) {\n        baseUrlParts = baseUrl.match(urlPartsRegex);\n        if (!baseUrlParts) {\n            throw new Error(\"Could not parse page url - '\"+baseUrl+\"'\");\n        }\n        urlParts[1] = urlParts[1] || baseUrlParts[1] || \"\";\n        if (!urlParts[2]) {\n            urlParts[3] = baseUrlParts[3] + urlParts[3];\n        }\n    }\n\n    if (urlParts[3]) {\n        directories = urlParts[3].replace(/\\\\/g, \"/\").split(\"/\");\n\n        // extract out . before .. so .. doesn't absorb a non-directory\n        for(i = 0; i < directories.length; i++) {\n            if (directories[i] === \".\") {\n                directories.splice(i, 1);\n                i -= 1;\n            }\n        }\n\n        for(i = 0; i < directories.length; i++) {\n            if (directories[i] === \"..\" && i > 0) {\n                directories.splice(i-1, 2);\n                i -= 2;\n            }\n        }\n    }\n\n    returner.hostPart = urlParts[1];\n    returner.directories = directories;\n    returner.path = urlParts[1] + directories.join(\"/\");\n    returner.fileUrl = returner.path + (urlParts[4] || \"\");\n    returner.url = returner.fileUrl + (urlParts[5] || \"\");\n    return returner;\n}\n\nfunction pathDiff(url, baseUrl) {\n    // diff between two paths to create a relative path\n\n    var urlParts = extractUrlParts(url),\n        baseUrlParts = extractUrlParts(baseUrl),\n        i, max, urlDirectories, baseUrlDirectories, diff = \"\";\n    if (urlParts.hostPart !== baseUrlParts.hostPart) {\n        return \"\";\n    }\n    max = Math.max(baseUrlParts.directories.length, urlParts.directories.length);\n    for(i = 0; i < max; i++) {\n        if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; }\n    }\n    baseUrlDirectories = baseUrlParts.directories.slice(i);\n    urlDirectories = urlParts.directories.slice(i);\n    for(i = 0; i < baseUrlDirectories.length-1; i++) {\n        diff += \"../\";\n    }\n    for(i = 0; i < urlDirectories.length-1; i++) {\n        diff += urlDirectories[i] + \"/\";\n    }\n    return diff;\n}\n\nfunction getXMLHttpRequest() {\n    if (window.XMLHttpRequest && (window.location.protocol !== \"file:\" || !window.ActiveXObject)) {\n        return new XMLHttpRequest();\n    } else {\n        try {\n            /*global ActiveXObject */\n            return new ActiveXObject(\"Microsoft.XMLHTTP\");\n        } catch (e) {\n            log(\"browser doesn't support AJAX.\", logLevel.errors);\n            return null;\n        }\n    }\n}\n\nfunction doXHR(url, type, callback, errback) {\n    var xhr = getXMLHttpRequest();\n    var async = isFileProtocol ? less.fileAsync : less.async;\n\n    if (typeof(xhr.overrideMimeType) === 'function') {\n        xhr.overrideMimeType('text/css');\n    }\n    log(\"XHR: Getting '\" + url + \"'\", logLevel.debug);\n    xhr.open('GET', url, async);\n    xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5');\n    xhr.send(null);\n\n    function handleResponse(xhr, callback, errback) {\n        if (xhr.status >= 200 && xhr.status < 300) {\n            callback(xhr.responseText,\n                xhr.getResponseHeader(\"Last-Modified\"));\n        } else if (typeof(errback) === 'function') {\n            errback(xhr.status, url);\n        }\n    }\n\n    if (isFileProtocol && !less.fileAsync) {\n        if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) {\n            callback(xhr.responseText);\n        } else {\n            errback(xhr.status, url);\n        }\n    } else if (async) {\n        xhr.onreadystatechange = function () {\n            if (xhr.readyState == 4) {\n                handleResponse(xhr, callback, errback);\n            }\n        };\n    } else {\n        handleResponse(xhr, callback, errback);\n    }\n}\n\nfunction loadFile(originalHref, currentFileInfo, callback, env, modifyVars) {\n\n    if (currentFileInfo && currentFileInfo.currentDirectory && !/^([a-z-]+:)?\\//.test(originalHref)) {\n        originalHref = currentFileInfo.currentDirectory + originalHref;\n    }\n\n    // sheet may be set to the stylesheet for the initial load or a collection of properties including\n    // some env variables for imports\n    var hrefParts = extractUrlParts(originalHref, window.location.href);\n    var href      = hrefParts.url;\n    var newFileInfo = {\n        currentDirectory: hrefParts.path,\n        filename: href\n    };\n\n    if (currentFileInfo) {\n        newFileInfo.entryPath = currentFileInfo.entryPath;\n        newFileInfo.rootpath = currentFileInfo.rootpath;\n        newFileInfo.rootFilename = currentFileInfo.rootFilename;\n        newFileInfo.relativeUrls = currentFileInfo.relativeUrls;\n    } else {\n        newFileInfo.entryPath = hrefParts.path;\n        newFileInfo.rootpath = less.rootpath || hrefParts.path;\n        newFileInfo.rootFilename = href;\n        newFileInfo.relativeUrls = env.relativeUrls;\n    }\n\n    if (newFileInfo.relativeUrls) {\n        if (env.rootpath) {\n            newFileInfo.rootpath = extractUrlParts(env.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path;\n        } else {\n            newFileInfo.rootpath = hrefParts.path;\n        }\n    }\n\n    if (env.useFileCache && fileCache[href]) {\n        try {\n            var lessText = fileCache[href];\n            callback(null, lessText, href, newFileInfo, { lastModified: new Date() });\n        } catch (e) {\n            callback(e, null, href);\n        }\n        return;\n    }\n\n    doXHR(href, env.mime, function (data, lastModified) {\n        // per file cache\n        fileCache[href] = data;\n\n        // Use remote copy (re-parse)\n        try {\n            callback(null, data, href, newFileInfo, { lastModified: lastModified });\n        } catch (e) {\n            callback(e, null, href);\n        }\n    }, function (status, url) {\n        callback({ type: 'File', message: \"'\" + url + \"' wasn't found (\" + status + \")\" }, null, href);\n    });\n}\n\nfunction loadStyleSheet(sheet, callback, reload, remaining, modifyVars) {\n\n    var env = new less.tree.parseEnv(less);\n    env.mime = sheet.type;\n\n    if (modifyVars || less.globalVars) {\n        env.useFileCache = true;\n    }\n\n    loadFile(sheet.href, null, function(e, data, path, newFileInfo, webInfo) {\n\n        if (webInfo) {\n            webInfo.remaining = remaining;\n\n            var css       = cache && cache.getItem(path),\n                timestamp = cache && cache.getItem(path + ':timestamp');\n\n            if (!reload && timestamp && webInfo.lastModified &&\n                (new(Date)(webInfo.lastModified).valueOf() ===\n                    new(Date)(timestamp).valueOf())) {\n                // Use local copy\n                createCSS(css, sheet);\n                webInfo.local = true;\n                callback(null, null, data, sheet, webInfo, path);\n                return;\n            }\n        }\n\n        //TODO add tests around how this behaves when reloading\n        removeError(path);\n\n        if (data) {\n            env.currentFileInfo = newFileInfo;\n            new(less.Parser)(env).parse(data, function (e, root) {\n                if (e) { return callback(e, null, null, sheet); }\n                try {\n                    callback(e, root, data, sheet, webInfo, path);\n                } catch (e) {\n                    callback(e, null, null, sheet);\n                }\n            }, {modifyVars: modifyVars, globalVars: less.globalVars});\n        } else {\n            callback(e, null, null, sheet, webInfo, path);\n        }\n    }, env, modifyVars);\n}\n\nfunction loadStyleSheets(callback, reload, modifyVars) {\n    for (var i = 0; i < less.sheets.length; i++) {\n        loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1), modifyVars);\n    }\n}\n\nfunction initRunningMode(){\n    if (less.env === 'development') {\n        less.optimization = 0;\n        less.watchTimer = setInterval(function () {\n            if (less.watchMode) {\n                loadStyleSheets(function (e, root, _, sheet, env) {\n                    if (e) {\n                        error(e, sheet.href);\n                    } else if (root) {\n                        var styles = root.toCSS(less);\n                        styles = postProcessCSS(styles);\n                        createCSS(styles, sheet, env.lastModified);\n                    }\n                });\n            }\n        }, less.poll);\n    } else {\n        less.optimization = 3;\n    }\n}\n\n\n\n//\n// Watch mode\n//\nless.watch   = function () {\n    if (!less.watchMode ){\n        less.env = 'development';\n         initRunningMode();\n    }\n    this.watchMode = true;\n    return true;\n};\n\nless.unwatch = function () {clearInterval(less.watchTimer); this.watchMode = false; return false; };\n\nif (/!watch/.test(location.hash)) {\n    less.watch();\n}\n\nif (less.env != 'development') {\n    try {\n        cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage;\n    } catch (_) {}\n}\n\n//\n// Get all <link> tags with the 'rel' attribute set to \"stylesheet/less\"\n//\nvar links = document.getElementsByTagName('link');\n\nless.sheets = [];\n\nfor (var i = 0; i < links.length; i++) {\n    if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) &&\n       (links[i].type.match(typePattern)))) {\n        less.sheets.push(links[i]);\n    }\n}\n\n//\n// With this function, it's possible to alter variables and re-render\n// CSS without reloading less-files\n//\nless.modifyVars = function(record) {\n    less.refresh(false, record);\n};\n\nless.refresh = function (reload, modifyVars) {\n    var startTime, endTime;\n    startTime = endTime = new Date();\n\n    loadStyleSheets(function (e, root, _, sheet, env) {\n        if (e) {\n            return error(e, sheet.href);\n        }\n        if (env.local) {\n            log(\"loading \" + sheet.href + \" from cache.\", logLevel.info);\n        } else {\n            log(\"parsed \" + sheet.href + \" successfully.\", logLevel.debug);\n            var styles = root.toCSS(less);\n            styles = postProcessCSS(styles);\n            createCSS(styles, sheet, env.lastModified);\n        }\n        log(\"css for \" + sheet.href + \" generated in \" + (new Date() - endTime) + 'ms', logLevel.info);\n        if (env.remaining === 0) {\n            log(\"less has finished. css generated in \" + (new Date() - startTime) + 'ms', logLevel.info);\n        }\n        endTime = new Date();\n    }, reload, modifyVars);\n\n    loadStyles(modifyVars);\n};\n\nless.refreshStyles = loadStyles;\n\nless.Parser.fileLoader = loadFile;\n\nless.refresh(less.env === 'development');\n\n// amd.js\n//\n// Define Less as an AMD module.\nif (typeof define === \"function\" && define.amd) {\n    define(function () { return less; } );\n}\n\n})(window);"
  },
  {
    "path": "commons-api2doc/src/main/resources/static/api2doc/test.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Api2Doc 接口测试</title>\n\n    <!--\n        使用 rem 布局，使 H5 页面能适配不同设备屏幕尺寸\n        flexible-lite-1.0.js 用于计算 html 根元素的 font-size 大小\n        然后 css 或 less 中所有的尺寸值一定要用 rem 单位，而不是 px 或其它单位。\n    -->\n    <meta charset=\"UTF-8\" name=\"viewport\"\n          content=\"width=device-width,initial-scale=1,user-scalable=0\"/>\n    <script src=\"./flexible-lite/flexible-lite-1.0.js\"></script>\n    <script type=\"text/javascript\">\n        flex(1000);\n    </script>\n\n    <!-- 使用了 element-ui 实现一些控件，element-ui 又用了 vue，因此都要引用 -->\n    <link href=\"./element-ui/element-ui.min.css\" rel=\"stylesheet\">\n    <script src=\"./vue/vue-2.5.10.min.js\"></script>\n    <script src=\"./element-ui/element-ui.min.js\"></script>\n\n    <!-- 使用 jQuery 来操作页面元素，ajax 加载数据。 -->\n    <script src=\"./jquery/jquery-3.3.1.min.js\"></script>\n\n    <!--\n            务必确保在 less.js 之前加载你的样式表。\n            如果加载多个 .less 样式表文件，每个文件都会被单独编译。\n            因此，一个文件中所定义的任何变量、mixin 或命名空间都无法在其它文件中访问。\n    -->\n    <link href=\"./css/test.less\" rel=\"stylesheet/less\" type=\"text/css\">\n    <script src=\"./less/less-1.7.0.js\" type=\"text/javascript\"></script>\n\n    <!-- 使代码根据语法显示样式（如：高亮等） -->\n    <link rel=\"stylesheet\"\n          href=\"//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css\">\n    <script src=\"//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js\"></script>\n    <script type=\"text/javascript\">\n        hljs.initHighlightingOnLoad();\n    </script>\n\n</head>\n<body>\n\n<div id=\"app\" class=\"test-app\" v-loading.fullscreen.lock=\"loading\"\n     element-loading-text=\"拼命加载中...\">\n\n    <h2>XXX接口测试</h2>\n\n    <div class=\"request\">\n        <div class=\"request-method\">\n            <el-select v-model=\"apiInfo.defaultMethod\">\n                <el-option v-for=\"item in apiInfo.methods\"\n                           :key=\"item\" :label=\"item\" :value=\"item\">\n                </el-option>\n            </el-select>\n        </div>\n        <div class=\"request-url\">\n            <el-input v-model=\"apiInfo.url\" placeholder=\"请输入URL\"></el-input>\n        </div>\n        <div class=\"clear\"></div>\n    </div>\n\n    <div class=\"items\">\n        <h3>请求参数</h3>\n        <div class=\"items-desc\">\n            如果是 POST 方法，这些请求参数会放在 Body 中；\n            如果是其它方法（如：GET / PUT / DELETE / PATCH 等），则会放在 URL 的 Query 串中。\n        </div>\n        <div class=\"item-list\">\n            <div class=\"item\" v-for=\"item in apiInfo.params\">\n                <div class=\"item-key\">\n                    <el-input v-model=\"item.key\" placeholder=\"参数名\"/>\n                </div>\n                <div class=\"item-split\">：</div>\n                <div class=\"item-value\">\n                    <el-input v-model=\"item.value\" placeholder=\"参数值\"/>\n                </div>\n                <div class=\"clear\"></div>\n            </div>\n        </div>\n        <div class=\"item-new\">\n            <el-button type=\"primary\" @click=\"newParam()\">+</el-button>\n        </div>\n        <div class=\"clear\"></div>\n    </div>\n\n    <div class=\"items\">\n        <h3>请求Header</h3>\n        <div class=\"item-list\">\n            <div class=\"item\" v-for=\"item in apiInfo.headers\">\n                <div class=\"item-key\">\n                    <el-input v-model=\"item.key\" placeholder=\"参数名\"/>\n                </div>\n                <div class=\"item-split\">：</div>\n                <div class=\"item-value\">\n                    <el-input v-model=\"item.value\" placeholder=\"参数值\"/>\n                </div>\n                <div class=\"clear\"></div>\n            </div>\n        </div>\n        <div class=\"item-new\">\n            <el-button type=\"primary\" @click=\"newHeader()\">+</el-button>\n        </div>\n        <div class=\"clear\"></div>\n    </div>\n\n    <div class=\"send\">\n        <div class=\"send-button\">\n            <el-button type=\"primary\">发送</el-button>\n        </div>\n        <div class=\"clear\"></div>\n    </div>\n\n    <div class=\"response\">\n        <h3>返回结果</h3>\n        <pre class=\"response-code\"><code class=\"language-json hljs\">\n{\n  \"requestId\" : \"a22c721867984258846686b89dbf82db\",\n  \"serverTime\" : 1524039750370,\n  \"spendTime\" : 5,\n  \"resultCode\" : \"success\"\n}\n        </code></pre>\n    </div>\n\n    <div class=\"output\">\n        <h3>输出日志</h3>\n        <pre class=\"output-logs\">\n\n  .   ____          _            __ _ _\n /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\\n( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\\n \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )\n  '  |____| .__|_| |_|_| |_\\__, | / / / /\n =========|_|==============|___/=/_/_/_/\n :: Spring Boot ::        (v1.5.9.RELEASE)\n\n2018-04-26 10:42:19.193  INFO 2328 --- [           main] c.terran4j.demo.api2doc.Api2DocDemoApp   : Starting Api2DocDemoApp on DESKTOP-32DF0L3 with PID 2328 (C:\\Users\\jiangwei\\IdeaProjects\\commons\\commons-api2doc\\target\\test-classes started by jiangwei in C:\\Users\\jiangwei\\IdeaProjects\\commons)\n2018-04-26 10:42:19.200  INFO 2328 --- [           main] c.terran4j.demo.api2doc.Api2DocDemoApp   : No active profile set, falling back to default profiles: default\n2018-04-26 10:42:19.479  INFO 2328 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4a22f9e2: startup date [Thu Apr 26 10:42:19 CST 2018]; root of context hierarchy\n2018-04-26 10:42:24.188  INFO 2328 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'api2DocService' of type [com.terran4j.commons.api2doc.impl.Api2DocService] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)\n2018-04-26 10:42:26.506  INFO 2328 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)\n2018-04-26 10:42:26.529  INFO 2328 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\n2018-04-26 10:42:26.531  INFO 2328 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23\n2018-04-26 10:42:27.034  INFO 2328 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext\n        </pre>\n    </div>\n\n</div>\n<script type=\"text/javascript\">\n    // 如果网速不给力导致加载太慢时，\n    // 会让页面先出现一个“拼命加载中...”的提示，\n    // 加载完成后再显示文档内容。\n    new Vue({\n        el: '#app',\n        data: function () {\n            return {\n                loading: true,\n                apiInfo: {\n                    methods: ['GET', 'POST'],\n                    defaultMethod: 'GET',\n                    url: \"http://localhost:8080/\",\n                    params: [\n                        {key: \"k1\", value: \"v1\"},\n                        {key: \"k2\", value: \"v2\"},\n                    ],\n                    headers: [\n                        {key: \"h1\", value: \"v1\"},\n                        {key: \"h2\", value: \"v2\"},\n                    ]\n                }\n            }\n        },\n        methods: {\n            newParam: function () {\n                console.log(\"newParam\");\n                this.apiInfo.params.push({key: \"\", value: \"\"});\n            },\n            newHeader: function () {\n                console.log(\"newHeader\");\n                this.apiInfo.headers.push({key: \"\", value: \"\"});\n            }\n        },\n        created: function () {\n            var _self = this;\n            jQuery.ajax({\n                type: 'GET',\n                url: '/api2doc/meta/apiInfo/params/requestParam',\n                success: function (data) {\n                    console.log(data);\n                    _self.apiInfo = JSON.stringify(data).data;\n                    _self.loading = false;\n                }\n            });\n        }\n    });\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "commons-api2doc/src/main/resources/static/api2doc/vue/vue-2.5.10.js",
    "content": "/*!\n * Vue.js v2.5.10\n * (c) 2014-2017 Evan You\n * Released under the MIT License.\n */\n(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.Vue = factory());\n}(this, (function () { 'use strict';\n\n/*  */\n\nvar emptyObject = Object.freeze({});\n\n// these helpers produces better vm code in JS engines due to their\n// explicitness and function inlining\nfunction isUndef (v) {\n  return v === undefined || v === null\n}\n\nfunction isDef (v) {\n  return v !== undefined && v !== null\n}\n\nfunction isTrue (v) {\n  return v === true\n}\n\nfunction isFalse (v) {\n  return v === false\n}\n\n/**\n * Check if value is primitive\n */\nfunction isPrimitive (value) {\n  return (\n    typeof value === 'string' ||\n    typeof value === 'number' ||\n    typeof value === 'boolean'\n  )\n}\n\n/**\n * Quick object check - this is primarily used to tell\n * Objects from primitive values when we know the value\n * is a JSON-compliant type.\n */\nfunction isObject (obj) {\n  return obj !== null && typeof obj === 'object'\n}\n\n/**\n * Get the raw type string of a value e.g. [object Object]\n */\nvar _toString = Object.prototype.toString;\n\nfunction toRawType (value) {\n  return _toString.call(value).slice(8, -1)\n}\n\n/**\n * Strict object type check. Only returns true\n * for plain JavaScript objects.\n */\nfunction isPlainObject (obj) {\n  return _toString.call(obj) === '[object Object]'\n}\n\nfunction isRegExp (v) {\n  return _toString.call(v) === '[object RegExp]'\n}\n\n/**\n * Check if val is a valid array index.\n */\nfunction isValidArrayIndex (val) {\n  var n = parseFloat(String(val));\n  return n >= 0 && Math.floor(n) === n && isFinite(val)\n}\n\n/**\n * Convert a value to a string that is actually rendered.\n */\nfunction toString (val) {\n  return val == null\n    ? ''\n    : typeof val === 'object'\n      ? JSON.stringify(val, null, 2)\n      : String(val)\n}\n\n/**\n * Convert a input value to a number for persistence.\n * If the conversion fails, return original string.\n */\nfunction toNumber (val) {\n  var n = parseFloat(val);\n  return isNaN(n) ? val : n\n}\n\n/**\n * Make a map and return a function for checking if a key\n * is in that map.\n */\nfunction makeMap (\n  str,\n  expectsLowerCase\n) {\n  var map = Object.create(null);\n  var list = str.split(',');\n  for (var i = 0; i < list.length; i++) {\n    map[list[i]] = true;\n  }\n  return expectsLowerCase\n    ? function (val) { return map[val.toLowerCase()]; }\n    : function (val) { return map[val]; }\n}\n\n/**\n * Check if a tag is a built-in tag.\n */\nvar isBuiltInTag = makeMap('slot,component', true);\n\n/**\n * Check if a attribute is a reserved attribute.\n */\nvar isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');\n\n/**\n * Remove an item from an array\n */\nfunction remove (arr, item) {\n  if (arr.length) {\n    var index = arr.indexOf(item);\n    if (index > -1) {\n      return arr.splice(index, 1)\n    }\n  }\n}\n\n/**\n * Check whether the object has the property.\n */\nvar hasOwnProperty = Object.prototype.hasOwnProperty;\nfunction hasOwn (obj, key) {\n  return hasOwnProperty.call(obj, key)\n}\n\n/**\n * Create a cached version of a pure function.\n */\nfunction cached (fn) {\n  var cache = Object.create(null);\n  return (function cachedFn (str) {\n    var hit = cache[str];\n    return hit || (cache[str] = fn(str))\n  })\n}\n\n/**\n * Camelize a hyphen-delimited string.\n */\nvar camelizeRE = /-(\\w)/g;\nvar camelize = cached(function (str) {\n  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })\n});\n\n/**\n * Capitalize a string.\n */\nvar capitalize = cached(function (str) {\n  return str.charAt(0).toUpperCase() + str.slice(1)\n});\n\n/**\n * Hyphenate a camelCase string.\n */\nvar hyphenateRE = /\\B([A-Z])/g;\nvar hyphenate = cached(function (str) {\n  return str.replace(hyphenateRE, '-$1').toLowerCase()\n});\n\n/**\n * Simple bind, faster than native\n */\nfunction bind (fn, ctx) {\n  function boundFn (a) {\n    var l = arguments.length;\n    return l\n      ? l > 1\n        ? fn.apply(ctx, arguments)\n        : fn.call(ctx, a)\n      : fn.call(ctx)\n  }\n  // record original fn length\n  boundFn._length = fn.length;\n  return boundFn\n}\n\n/**\n * Convert an Array-like object to a real Array.\n */\nfunction toArray (list, start) {\n  start = start || 0;\n  var i = list.length - start;\n  var ret = new Array(i);\n  while (i--) {\n    ret[i] = list[i + start];\n  }\n  return ret\n}\n\n/**\n * Mix properties into target object.\n */\nfunction extend (to, _from) {\n  for (var key in _from) {\n    to[key] = _from[key];\n  }\n  return to\n}\n\n/**\n * Merge an Array of Objects into a single Object.\n */\nfunction toObject (arr) {\n  var res = {};\n  for (var i = 0; i < arr.length; i++) {\n    if (arr[i]) {\n      extend(res, arr[i]);\n    }\n  }\n  return res\n}\n\n/**\n * Perform no operation.\n * Stubbing args to make Flow happy without leaving useless transpiled code\n * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/)\n */\nfunction noop (a, b, c) {}\n\n/**\n * Always return false.\n */\nvar no = function (a, b, c) { return false; };\n\n/**\n * Return same value\n */\nvar identity = function (_) { return _; };\n\n/**\n * Generate a static keys string from compiler modules.\n */\nfunction genStaticKeys (modules) {\n  return modules.reduce(function (keys, m) {\n    return keys.concat(m.staticKeys || [])\n  }, []).join(',')\n}\n\n/**\n * Check if two values are loosely equal - that is,\n * if they are plain objects, do they have the same shape?\n */\nfunction looseEqual (a, b) {\n  if (a === b) { return true }\n  var isObjectA = isObject(a);\n  var isObjectB = isObject(b);\n  if (isObjectA && isObjectB) {\n    try {\n      var isArrayA = Array.isArray(a);\n      var isArrayB = Array.isArray(b);\n      if (isArrayA && isArrayB) {\n        return a.length === b.length && a.every(function (e, i) {\n          return looseEqual(e, b[i])\n        })\n      } else if (!isArrayA && !isArrayB) {\n        var keysA = Object.keys(a);\n        var keysB = Object.keys(b);\n        return keysA.length === keysB.length && keysA.every(function (key) {\n          return looseEqual(a[key], b[key])\n        })\n      } else {\n        /* istanbul ignore next */\n        return false\n      }\n    } catch (e) {\n      /* istanbul ignore next */\n      return false\n    }\n  } else if (!isObjectA && !isObjectB) {\n    return String(a) === String(b)\n  } else {\n    return false\n  }\n}\n\nfunction looseIndexOf (arr, val) {\n  for (var i = 0; i < arr.length; i++) {\n    if (looseEqual(arr[i], val)) { return i }\n  }\n  return -1\n}\n\n/**\n * Ensure a function is called only once.\n */\nfunction once (fn) {\n  var called = false;\n  return function () {\n    if (!called) {\n      called = true;\n      fn.apply(this, arguments);\n    }\n  }\n}\n\nvar SSR_ATTR = 'data-server-rendered';\n\nvar ASSET_TYPES = [\n  'component',\n  'directive',\n  'filter'\n];\n\nvar LIFECYCLE_HOOKS = [\n  'beforeCreate',\n  'created',\n  'beforeMount',\n  'mounted',\n  'beforeUpdate',\n  'updated',\n  'beforeDestroy',\n  'destroyed',\n  'activated',\n  'deactivated',\n  'errorCaptured'\n];\n\n/*  */\n\nvar config = ({\n  /**\n   * Option merge strategies (used in core/util/options)\n   */\n  optionMergeStrategies: Object.create(null),\n\n  /**\n   * Whether to suppress warnings.\n   */\n  silent: false,\n\n  /**\n   * Show production mode tip message on boot?\n   */\n  productionTip: \"development\" !== 'production',\n\n  /**\n   * Whether to enable devtools\n   */\n  devtools: \"development\" !== 'production',\n\n  /**\n   * Whether to record perf\n   */\n  performance: false,\n\n  /**\n   * Error handler for watcher errors\n   */\n  errorHandler: null,\n\n  /**\n   * Warn handler for watcher warns\n   */\n  warnHandler: null,\n\n  /**\n   * Ignore certain custom elements\n   */\n  ignoredElements: [],\n\n  /**\n   * Custom user key aliases for v-on\n   */\n  keyCodes: Object.create(null),\n\n  /**\n   * Check if a tag is reserved so that it cannot be registered as a\n   * component. This is platform-dependent and may be overwritten.\n   */\n  isReservedTag: no,\n\n  /**\n   * Check if an attribute is reserved so that it cannot be used as a component\n   * prop. This is platform-dependent and may be overwritten.\n   */\n  isReservedAttr: no,\n\n  /**\n   * Check if a tag is an unknown element.\n   * Platform-dependent.\n   */\n  isUnknownElement: no,\n\n  /**\n   * Get the namespace of an element\n   */\n  getTagNamespace: noop,\n\n  /**\n   * Parse the real tag name for the specific platform.\n   */\n  parsePlatformTagName: identity,\n\n  /**\n   * Check if an attribute must be bound using property, e.g. value\n   * Platform-dependent.\n   */\n  mustUseProp: no,\n\n  /**\n   * Exposed for legacy reasons\n   */\n  _lifecycleHooks: LIFECYCLE_HOOKS\n});\n\n/*  */\n\n/**\n * Check if a string starts with $ or _\n */\nfunction isReserved (str) {\n  var c = (str + '').charCodeAt(0);\n  return c === 0x24 || c === 0x5F\n}\n\n/**\n * Define a property.\n */\nfunction def (obj, key, val, enumerable) {\n  Object.defineProperty(obj, key, {\n    value: val,\n    enumerable: !!enumerable,\n    writable: true,\n    configurable: true\n  });\n}\n\n/**\n * Parse simple path.\n */\nvar bailRE = /[^\\w.$]/;\nfunction parsePath (path) {\n  if (bailRE.test(path)) {\n    return\n  }\n  var segments = path.split('.');\n  return function (obj) {\n    for (var i = 0; i < segments.length; i++) {\n      if (!obj) { return }\n      obj = obj[segments[i]];\n    }\n    return obj\n  }\n}\n\n/*  */\n\n\n// can we use __proto__?\nvar hasProto = '__proto__' in {};\n\n// Browser environment sniffing\nvar inBrowser = typeof window !== 'undefined';\nvar inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform;\nvar weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();\nvar UA = inBrowser && window.navigator.userAgent.toLowerCase();\nvar isIE = UA && /msie|trident/.test(UA);\nvar isIE9 = UA && UA.indexOf('msie 9.0') > 0;\nvar isEdge = UA && UA.indexOf('edge/') > 0;\nvar isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android');\nvar isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios');\nvar isChrome = UA && /chrome\\/\\d+/.test(UA) && !isEdge;\n\n// Firefox has a \"watch\" function on Object.prototype...\nvar nativeWatch = ({}).watch;\n\nvar supportsPassive = false;\nif (inBrowser) {\n  try {\n    var opts = {};\n    Object.defineProperty(opts, 'passive', ({\n      get: function get () {\n        /* istanbul ignore next */\n        supportsPassive = true;\n      }\n    })); // https://github.com/facebook/flow/issues/285\n    window.addEventListener('test-passive', null, opts);\n  } catch (e) {}\n}\n\n// this needs to be lazy-evaled because vue may be required before\n// vue-server-renderer can set VUE_ENV\nvar _isServer;\nvar isServerRendering = function () {\n  if (_isServer === undefined) {\n    /* istanbul ignore if */\n    if (!inBrowser && typeof global !== 'undefined') {\n      // detect presence of vue-server-renderer and avoid\n      // Webpack shimming the process\n      _isServer = global['process'].env.VUE_ENV === 'server';\n    } else {\n      _isServer = false;\n    }\n  }\n  return _isServer\n};\n\n// detect devtools\nvar devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;\n\n/* istanbul ignore next */\nfunction isNative (Ctor) {\n  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())\n}\n\nvar hasSymbol =\n  typeof Symbol !== 'undefined' && isNative(Symbol) &&\n  typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys);\n\nvar _Set;\n/* istanbul ignore if */ // $flow-disable-line\nif (typeof Set !== 'undefined' && isNative(Set)) {\n  // use native Set when available.\n  _Set = Set;\n} else {\n  // a non-standard Set polyfill that only works with primitive keys.\n  _Set = (function () {\n    function Set () {\n      this.set = Object.create(null);\n    }\n    Set.prototype.has = function has (key) {\n      return this.set[key] === true\n    };\n    Set.prototype.add = function add (key) {\n      this.set[key] = true;\n    };\n    Set.prototype.clear = function clear () {\n      this.set = Object.create(null);\n    };\n\n    return Set;\n  }());\n}\n\n/*  */\n\nvar warn = noop;\nvar tip = noop;\nvar generateComponentTrace = (noop); // work around flow check\nvar formatComponentName = (noop);\n\n{\n  var hasConsole = typeof console !== 'undefined';\n  var classifyRE = /(?:^|[-_])(\\w)/g;\n  var classify = function (str) { return str\n    .replace(classifyRE, function (c) { return c.toUpperCase(); })\n    .replace(/[-_]/g, ''); };\n\n  warn = function (msg, vm) {\n    var trace = vm ? generateComponentTrace(vm) : '';\n\n    if (config.warnHandler) {\n      config.warnHandler.call(null, msg, vm, trace);\n    } else if (hasConsole && (!config.silent)) {\n      console.error((\"[Vue warn]: \" + msg + trace));\n    }\n  };\n\n  tip = function (msg, vm) {\n    if (hasConsole && (!config.silent)) {\n      console.warn(\"[Vue tip]: \" + msg + (\n        vm ? generateComponentTrace(vm) : ''\n      ));\n    }\n  };\n\n  formatComponentName = function (vm, includeFile) {\n    if (vm.$root === vm) {\n      return '<Root>'\n    }\n    var options = typeof vm === 'function' && vm.cid != null\n      ? vm.options\n      : vm._isVue\n        ? vm.$options || vm.constructor.options\n        : vm || {};\n    var name = options.name || options._componentTag;\n    var file = options.__file;\n    if (!name && file) {\n      var match = file.match(/([^/\\\\]+)\\.vue$/);\n      name = match && match[1];\n    }\n\n    return (\n      (name ? (\"<\" + (classify(name)) + \">\") : \"<Anonymous>\") +\n      (file && includeFile !== false ? (\" at \" + file) : '')\n    )\n  };\n\n  var repeat = function (str, n) {\n    var res = '';\n    while (n) {\n      if (n % 2 === 1) { res += str; }\n      if (n > 1) { str += str; }\n      n >>= 1;\n    }\n    return res\n  };\n\n  generateComponentTrace = function (vm) {\n    if (vm._isVue && vm.$parent) {\n      var tree = [];\n      var currentRecursiveSequence = 0;\n      while (vm) {\n        if (tree.length > 0) {\n          var last = tree[tree.length - 1];\n          if (last.constructor === vm.constructor) {\n            currentRecursiveSequence++;\n            vm = vm.$parent;\n            continue\n          } else if (currentRecursiveSequence > 0) {\n            tree[tree.length - 1] = [last, currentRecursiveSequence];\n            currentRecursiveSequence = 0;\n          }\n        }\n        tree.push(vm);\n        vm = vm.$parent;\n      }\n      return '\\n\\nfound in\\n\\n' + tree\n        .map(function (vm, i) { return (\"\" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm)\n            ? ((formatComponentName(vm[0])) + \"... (\" + (vm[1]) + \" recursive calls)\")\n            : formatComponentName(vm))); })\n        .join('\\n')\n    } else {\n      return (\"\\n\\n(found in \" + (formatComponentName(vm)) + \")\")\n    }\n  };\n}\n\n/*  */\n\n\nvar uid = 0;\n\n/**\n * A dep is an observable that can have multiple\n * directives subscribing to it.\n */\nvar Dep = function Dep () {\n  this.id = uid++;\n  this.subs = [];\n};\n\nDep.prototype.addSub = function addSub (sub) {\n  this.subs.push(sub);\n};\n\nDep.prototype.removeSub = function removeSub (sub) {\n  remove(this.subs, sub);\n};\n\nDep.prototype.depend = function depend () {\n  if (Dep.target) {\n    Dep.target.addDep(this);\n  }\n};\n\nDep.prototype.notify = function notify () {\n  // stabilize the subscriber list first\n  var subs = this.subs.slice();\n  for (var i = 0, l = subs.length; i < l; i++) {\n    subs[i].update();\n  }\n};\n\n// the current target watcher being evaluated.\n// this is globally unique because there could be only one\n// watcher being evaluated at any time.\nDep.target = null;\nvar targetStack = [];\n\nfunction pushTarget (_target) {\n  if (Dep.target) { targetStack.push(Dep.target); }\n  Dep.target = _target;\n}\n\nfunction popTarget () {\n  Dep.target = targetStack.pop();\n}\n\n/*  */\n\nvar VNode = function VNode (\n  tag,\n  data,\n  children,\n  text,\n  elm,\n  context,\n  componentOptions,\n  asyncFactory\n) {\n  this.tag = tag;\n  this.data = data;\n  this.children = children;\n  this.text = text;\n  this.elm = elm;\n  this.ns = undefined;\n  this.context = context;\n  this.fnContext = undefined;\n  this.fnOptions = undefined;\n  this.fnScopeId = undefined;\n  this.key = data && data.key;\n  this.componentOptions = componentOptions;\n  this.componentInstance = undefined;\n  this.parent = undefined;\n  this.raw = false;\n  this.isStatic = false;\n  this.isRootInsert = true;\n  this.isComment = false;\n  this.isCloned = false;\n  this.isOnce = false;\n  this.asyncFactory = asyncFactory;\n  this.asyncMeta = undefined;\n  this.isAsyncPlaceholder = false;\n};\n\nvar prototypeAccessors = { child: { configurable: true } };\n\n// DEPRECATED: alias for componentInstance for backwards compat.\n/* istanbul ignore next */\nprototypeAccessors.child.get = function () {\n  return this.componentInstance\n};\n\nObject.defineProperties( VNode.prototype, prototypeAccessors );\n\nvar createEmptyVNode = function (text) {\n  if ( text === void 0 ) text = '';\n\n  var node = new VNode();\n  node.text = text;\n  node.isComment = true;\n  return node\n};\n\nfunction createTextVNode (val) {\n  return new VNode(undefined, undefined, undefined, String(val))\n}\n\n// optimized shallow clone\n// used for static nodes and slot nodes because they may be reused across\n// multiple renders, cloning them avoids errors when DOM manipulations rely\n// on their elm reference.\nfunction cloneVNode (vnode, deep) {\n  var componentOptions = vnode.componentOptions;\n  var cloned = new VNode(\n    vnode.tag,\n    vnode.data,\n    vnode.children,\n    vnode.text,\n    vnode.elm,\n    vnode.context,\n    componentOptions,\n    vnode.asyncFactory\n  );\n  cloned.ns = vnode.ns;\n  cloned.isStatic = vnode.isStatic;\n  cloned.key = vnode.key;\n  cloned.isComment = vnode.isComment;\n  cloned.fnContext = vnode.fnContext;\n  cloned.fnOptions = vnode.fnOptions;\n  cloned.fnScopeId = vnode.fnScopeId;\n  cloned.isCloned = true;\n  if (deep) {\n    if (vnode.children) {\n      cloned.children = cloneVNodes(vnode.children, true);\n    }\n    if (componentOptions && componentOptions.children) {\n      componentOptions.children = cloneVNodes(componentOptions.children, true);\n    }\n  }\n  return cloned\n}\n\nfunction cloneVNodes (vnodes, deep) {\n  var len = vnodes.length;\n  var res = new Array(len);\n  for (var i = 0; i < len; i++) {\n    res[i] = cloneVNode(vnodes[i], deep);\n  }\n  return res\n}\n\n/*\n * not type checking this file because flow doesn't play well with\n * dynamically accessing methods on Array prototype\n */\n\nvar arrayProto = Array.prototype;\nvar arrayMethods = Object.create(arrayProto);[\n  'push',\n  'pop',\n  'shift',\n  'unshift',\n  'splice',\n  'sort',\n  'reverse'\n]\n.forEach(function (method) {\n  // cache original method\n  var original = arrayProto[method];\n  def(arrayMethods, method, function mutator () {\n    var args = [], len = arguments.length;\n    while ( len-- ) args[ len ] = arguments[ len ];\n\n    var result = original.apply(this, args);\n    var ob = this.__ob__;\n    var inserted;\n    switch (method) {\n      case 'push':\n      case 'unshift':\n        inserted = args;\n        break\n      case 'splice':\n        inserted = args.slice(2);\n        break\n    }\n    if (inserted) { ob.observeArray(inserted); }\n    // notify change\n    ob.dep.notify();\n    return result\n  });\n});\n\n/*  */\n\nvar arrayKeys = Object.getOwnPropertyNames(arrayMethods);\n\n/**\n * By default, when a reactive property is set, the new value is\n * also converted to become reactive. However when passing down props,\n * we don't want to force conversion because the value may be a nested value\n * under a frozen data structure. Converting it would defeat the optimization.\n */\nvar observerState = {\n  shouldConvert: true\n};\n\n/**\n * Observer class that are attached to each observed\n * object. Once attached, the observer converts target\n * object's property keys into getter/setters that\n * collect dependencies and dispatches updates.\n */\nvar Observer = function Observer (value) {\n  this.value = value;\n  this.dep = new Dep();\n  this.vmCount = 0;\n  def(value, '__ob__', this);\n  if (Array.isArray(value)) {\n    var augment = hasProto\n      ? protoAugment\n      : copyAugment;\n    augment(value, arrayMethods, arrayKeys);\n    this.observeArray(value);\n  } else {\n    this.walk(value);\n  }\n};\n\n/**\n * Walk through each property and convert them into\n * getter/setters. This method should only be called when\n * value type is Object.\n */\nObserver.prototype.walk = function walk (obj) {\n  var keys = Object.keys(obj);\n  for (var i = 0; i < keys.length; i++) {\n    defineReactive(obj, keys[i], obj[keys[i]]);\n  }\n};\n\n/**\n * Observe a list of Array items.\n */\nObserver.prototype.observeArray = function observeArray (items) {\n  for (var i = 0, l = items.length; i < l; i++) {\n    observe(items[i]);\n  }\n};\n\n// helpers\n\n/**\n * Augment an target Object or Array by intercepting\n * the prototype chain using __proto__\n */\nfunction protoAugment (target, src, keys) {\n  /* eslint-disable no-proto */\n  target.__proto__ = src;\n  /* eslint-enable no-proto */\n}\n\n/**\n * Augment an target Object or Array by defining\n * hidden properties.\n */\n/* istanbul ignore next */\nfunction copyAugment (target, src, keys) {\n  for (var i = 0, l = keys.length; i < l; i++) {\n    var key = keys[i];\n    def(target, key, src[key]);\n  }\n}\n\n/**\n * Attempt to create an observer instance for a value,\n * returns the new observer if successfully observed,\n * or the existing observer if the value already has one.\n */\nfunction observe (value, asRootData) {\n  if (!isObject(value) || value instanceof VNode) {\n    return\n  }\n  var ob;\n  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {\n    ob = value.__ob__;\n  } else if (\n    observerState.shouldConvert &&\n    !isServerRendering() &&\n    (Array.isArray(value) || isPlainObject(value)) &&\n    Object.isExtensible(value) &&\n    !value._isVue\n  ) {\n    ob = new Observer(value);\n  }\n  if (asRootData && ob) {\n    ob.vmCount++;\n  }\n  return ob\n}\n\n/**\n * Define a reactive property on an Object.\n */\nfunction defineReactive (\n  obj,\n  key,\n  val,\n  customSetter,\n  shallow\n) {\n  var dep = new Dep();\n\n  var property = Object.getOwnPropertyDescriptor(obj, key);\n  if (property && property.configurable === false) {\n    return\n  }\n\n  // cater for pre-defined getter/setters\n  var getter = property && property.get;\n  var setter = property && property.set;\n\n  var childOb = !shallow && observe(val);\n  Object.defineProperty(obj, key, {\n    enumerable: true,\n    configurable: true,\n    get: function reactiveGetter () {\n      var value = getter ? getter.call(obj) : val;\n      if (Dep.target) {\n        dep.depend();\n        if (childOb) {\n          childOb.dep.depend();\n          if (Array.isArray(value)) {\n            dependArray(value);\n          }\n        }\n      }\n      return value\n    },\n    set: function reactiveSetter (newVal) {\n      var value = getter ? getter.call(obj) : val;\n      /* eslint-disable no-self-compare */\n      if (newVal === value || (newVal !== newVal && value !== value)) {\n        return\n      }\n      /* eslint-enable no-self-compare */\n      if (\"development\" !== 'production' && customSetter) {\n        customSetter();\n      }\n      if (setter) {\n        setter.call(obj, newVal);\n      } else {\n        val = newVal;\n      }\n      childOb = !shallow && observe(newVal);\n      dep.notify();\n    }\n  });\n}\n\n/**\n * Set a property on an object. Adds the new property and\n * triggers change notification if the property doesn't\n * already exist.\n */\nfunction set (target, key, val) {\n  if (Array.isArray(target) && isValidArrayIndex(key)) {\n    target.length = Math.max(target.length, key);\n    target.splice(key, 1, val);\n    return val\n  }\n  if (key in target && !(key in Object.prototype)) {\n    target[key] = val;\n    return val\n  }\n  var ob = (target).__ob__;\n  if (target._isVue || (ob && ob.vmCount)) {\n    \"development\" !== 'production' && warn(\n      'Avoid adding reactive properties to a Vue instance or its root $data ' +\n      'at runtime - declare it upfront in the data option.'\n    );\n    return val\n  }\n  if (!ob) {\n    target[key] = val;\n    return val\n  }\n  defineReactive(ob.value, key, val);\n  ob.dep.notify();\n  return val\n}\n\n/**\n * Delete a property and trigger change if necessary.\n */\nfunction del (target, key) {\n  if (Array.isArray(target) && isValidArrayIndex(key)) {\n    target.splice(key, 1);\n    return\n  }\n  var ob = (target).__ob__;\n  if (target._isVue || (ob && ob.vmCount)) {\n    \"development\" !== 'production' && warn(\n      'Avoid deleting properties on a Vue instance or its root $data ' +\n      '- just set it to null.'\n    );\n    return\n  }\n  if (!hasOwn(target, key)) {\n    return\n  }\n  delete target[key];\n  if (!ob) {\n    return\n  }\n  ob.dep.notify();\n}\n\n/**\n * Collect dependencies on array elements when the array is touched, since\n * we cannot intercept array element access like property getters.\n */\nfunction dependArray (value) {\n  for (var e = (void 0), i = 0, l = value.length; i < l; i++) {\n    e = value[i];\n    e && e.__ob__ && e.__ob__.dep.depend();\n    if (Array.isArray(e)) {\n      dependArray(e);\n    }\n  }\n}\n\n/*  */\n\n/**\n * Option overwriting strategies are functions that handle\n * how to merge a parent option value and a child option\n * value into the final value.\n */\nvar strats = config.optionMergeStrategies;\n\n/**\n * Options with restrictions\n */\n{\n  strats.el = strats.propsData = function (parent, child, vm, key) {\n    if (!vm) {\n      warn(\n        \"option \\\"\" + key + \"\\\" can only be used during instance \" +\n        'creation with the `new` keyword.'\n      );\n    }\n    return defaultStrat(parent, child)\n  };\n}\n\n/**\n * Helper that recursively merges two data objects together.\n */\nfunction mergeData (to, from) {\n  if (!from) { return to }\n  var key, toVal, fromVal;\n  var keys = Object.keys(from);\n  for (var i = 0; i < keys.length; i++) {\n    key = keys[i];\n    toVal = to[key];\n    fromVal = from[key];\n    if (!hasOwn(to, key)) {\n      set(to, key, fromVal);\n    } else if (isPlainObject(toVal) && isPlainObject(fromVal)) {\n      mergeData(toVal, fromVal);\n    }\n  }\n  return to\n}\n\n/**\n * Data\n */\nfunction mergeDataOrFn (\n  parentVal,\n  childVal,\n  vm\n) {\n  if (!vm) {\n    // in a Vue.extend merge, both should be functions\n    if (!childVal) {\n      return parentVal\n    }\n    if (!parentVal) {\n      return childVal\n    }\n    // when parentVal & childVal are both present,\n    // we need to return a function that returns the\n    // merged result of both functions... no need to\n    // check if parentVal is a function here because\n    // it has to be a function to pass previous merges.\n    return function mergedDataFn () {\n      return mergeData(\n        typeof childVal === 'function' ? childVal.call(this, this) : childVal,\n        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal\n      )\n    }\n  } else {\n    return function mergedInstanceDataFn () {\n      // instance merge\n      var instanceData = typeof childVal === 'function'\n        ? childVal.call(vm, vm)\n        : childVal;\n      var defaultData = typeof parentVal === 'function'\n        ? parentVal.call(vm, vm)\n        : parentVal;\n      if (instanceData) {\n        return mergeData(instanceData, defaultData)\n      } else {\n        return defaultData\n      }\n    }\n  }\n}\n\nstrats.data = function (\n  parentVal,\n  childVal,\n  vm\n) {\n  if (!vm) {\n    if (childVal && typeof childVal !== 'function') {\n      \"development\" !== 'production' && warn(\n        'The \"data\" option should be a function ' +\n        'that returns a per-instance value in component ' +\n        'definitions.',\n        vm\n      );\n\n      return parentVal\n    }\n    return mergeDataOrFn(parentVal, childVal)\n  }\n\n  return mergeDataOrFn(parentVal, childVal, vm)\n};\n\n/**\n * Hooks and props are merged as arrays.\n */\nfunction mergeHook (\n  parentVal,\n  childVal\n) {\n  return childVal\n    ? parentVal\n      ? parentVal.concat(childVal)\n      : Array.isArray(childVal)\n        ? childVal\n        : [childVal]\n    : parentVal\n}\n\nLIFECYCLE_HOOKS.forEach(function (hook) {\n  strats[hook] = mergeHook;\n});\n\n/**\n * Assets\n *\n * When a vm is present (instance creation), we need to do\n * a three-way merge between constructor options, instance\n * options and parent options.\n */\nfunction mergeAssets (\n  parentVal,\n  childVal,\n  vm,\n  key\n) {\n  var res = Object.create(parentVal || null);\n  if (childVal) {\n    \"development\" !== 'production' && assertObjectType(key, childVal, vm);\n    return extend(res, childVal)\n  } else {\n    return res\n  }\n}\n\nASSET_TYPES.forEach(function (type) {\n  strats[type + 's'] = mergeAssets;\n});\n\n/**\n * Watchers.\n *\n * Watchers hashes should not overwrite one\n * another, so we merge them as arrays.\n */\nstrats.watch = function (\n  parentVal,\n  childVal,\n  vm,\n  key\n) {\n  // work around Firefox's Object.prototype.watch...\n  if (parentVal === nativeWatch) { parentVal = undefined; }\n  if (childVal === nativeWatch) { childVal = undefined; }\n  /* istanbul ignore if */\n  if (!childVal) { return Object.create(parentVal || null) }\n  {\n    assertObjectType(key, childVal, vm);\n  }\n  if (!parentVal) { return childVal }\n  var ret = {};\n  extend(ret, parentVal);\n  for (var key$1 in childVal) {\n    var parent = ret[key$1];\n    var child = childVal[key$1];\n    if (parent && !Array.isArray(parent)) {\n      parent = [parent];\n    }\n    ret[key$1] = parent\n      ? parent.concat(child)\n      : Array.isArray(child) ? child : [child];\n  }\n  return ret\n};\n\n/**\n * Other object hashes.\n */\nstrats.props =\nstrats.methods =\nstrats.inject =\nstrats.computed = function (\n  parentVal,\n  childVal,\n  vm,\n  key\n) {\n  if (childVal && \"development\" !== 'production') {\n    assertObjectType(key, childVal, vm);\n  }\n  if (!parentVal) { return childVal }\n  var ret = Object.create(null);\n  extend(ret, parentVal);\n  if (childVal) { extend(ret, childVal); }\n  return ret\n};\nstrats.provide = mergeDataOrFn;\n\n/**\n * Default strategy.\n */\nvar defaultStrat = function (parentVal, childVal) {\n  return childVal === undefined\n    ? parentVal\n    : childVal\n};\n\n/**\n * Validate component names\n */\nfunction checkComponents (options) {\n  for (var key in options.components) {\n    validateComponentName(key);\n  }\n}\n\nfunction validateComponentName (name) {\n  if (!/^[a-zA-Z][\\w-]*$/.test(name)) {\n    warn(\n      'Invalid component name: \"' + name + '\". Component names ' +\n      'can only contain alphanumeric characters and the hyphen, ' +\n      'and must start with a letter.'\n    );\n  }\n  var lower = name.toLowerCase();\n  if (isBuiltInTag(lower) || config.isReservedTag(lower)) {\n    warn(\n      'Do not use built-in or reserved HTML elements as component ' +\n      'id: ' + name\n    );\n  }\n}\n\n/**\n * Ensure all props option syntax are normalized into the\n * Object-based format.\n */\nfunction normalizeProps (options, vm) {\n  var props = options.props;\n  if (!props) { return }\n  var res = {};\n  var i, val, name;\n  if (Array.isArray(props)) {\n    i = props.length;\n    while (i--) {\n      val = props[i];\n      if (typeof val === 'string') {\n        name = camelize(val);\n        res[name] = { type: null };\n      } else {\n        warn('props must be strings when using array syntax.');\n      }\n    }\n  } else if (isPlainObject(props)) {\n    for (var key in props) {\n      val = props[key];\n      name = camelize(key);\n      res[name] = isPlainObject(val)\n        ? val\n        : { type: val };\n    }\n  } else {\n    warn(\n      \"Invalid value for option \\\"props\\\": expected an Array or an Object, \" +\n      \"but got \" + (toRawType(props)) + \".\",\n      vm\n    );\n  }\n  options.props = res;\n}\n\n/**\n * Normalize all injections into Object-based format\n */\nfunction normalizeInject (options, vm) {\n  var inject = options.inject;\n  var normalized = options.inject = {};\n  if (Array.isArray(inject)) {\n    for (var i = 0; i < inject.length; i++) {\n      normalized[inject[i]] = { from: inject[i] };\n    }\n  } else if (isPlainObject(inject)) {\n    for (var key in inject) {\n      var val = inject[key];\n      normalized[key] = isPlainObject(val)\n        ? extend({ from: key }, val)\n        : { from: val };\n    }\n  } else if (\"development\" !== 'production' && inject) {\n    warn(\n      \"Invalid value for option \\\"inject\\\": expected an Array or an Object, \" +\n      \"but got \" + (toRawType(inject)) + \".\",\n      vm\n    );\n  }\n}\n\n/**\n * Normalize raw function directives into object format.\n */\nfunction normalizeDirectives (options) {\n  var dirs = options.directives;\n  if (dirs) {\n    for (var key in dirs) {\n      var def = dirs[key];\n      if (typeof def === 'function') {\n        dirs[key] = { bind: def, update: def };\n      }\n    }\n  }\n}\n\nfunction assertObjectType (name, value, vm) {\n  if (!isPlainObject(value)) {\n    warn(\n      \"Invalid value for option \\\"\" + name + \"\\\": expected an Object, \" +\n      \"but got \" + (toRawType(value)) + \".\",\n      vm\n    );\n  }\n}\n\n/**\n * Merge two option objects into a new one.\n * Core utility used in both instantiation and inheritance.\n */\nfunction mergeOptions (\n  parent,\n  child,\n  vm\n) {\n  {\n    checkComponents(child);\n  }\n\n  if (typeof child === 'function') {\n    child = child.options;\n  }\n\n  normalizeProps(child, vm);\n  normalizeInject(child, vm);\n  normalizeDirectives(child);\n  var extendsFrom = child.extends;\n  if (extendsFrom) {\n    parent = mergeOptions(parent, extendsFrom, vm);\n  }\n  if (child.mixins) {\n    for (var i = 0, l = child.mixins.length; i < l; i++) {\n      parent = mergeOptions(parent, child.mixins[i], vm);\n    }\n  }\n  var options = {};\n  var key;\n  for (key in parent) {\n    mergeField(key);\n  }\n  for (key in child) {\n    if (!hasOwn(parent, key)) {\n      mergeField(key);\n    }\n  }\n  function mergeField (key) {\n    var strat = strats[key] || defaultStrat;\n    options[key] = strat(parent[key], child[key], vm, key);\n  }\n  return options\n}\n\n/**\n * Resolve an asset.\n * This function is used because child instances need access\n * to assets defined in its ancestor chain.\n */\nfunction resolveAsset (\n  options,\n  type,\n  id,\n  warnMissing\n) {\n  /* istanbul ignore if */\n  if (typeof id !== 'string') {\n    return\n  }\n  var assets = options[type];\n  // check local registration variations first\n  if (hasOwn(assets, id)) { return assets[id] }\n  var camelizedId = camelize(id);\n  if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }\n  var PascalCaseId = capitalize(camelizedId);\n  if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }\n  // fallback to prototype chain\n  var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];\n  if (\"development\" !== 'production' && warnMissing && !res) {\n    warn(\n      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,\n      options\n    );\n  }\n  return res\n}\n\n/*  */\n\nfunction validateProp (\n  key,\n  propOptions,\n  propsData,\n  vm\n) {\n  var prop = propOptions[key];\n  var absent = !hasOwn(propsData, key);\n  var value = propsData[key];\n  // handle boolean props\n  if (isType(Boolean, prop.type)) {\n    if (absent && !hasOwn(prop, 'default')) {\n      value = false;\n    } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {\n      value = true;\n    }\n  }\n  // check default value\n  if (value === undefined) {\n    value = getPropDefaultValue(vm, prop, key);\n    // since the default value is a fresh copy,\n    // make sure to observe it.\n    var prevShouldConvert = observerState.shouldConvert;\n    observerState.shouldConvert = true;\n    observe(value);\n    observerState.shouldConvert = prevShouldConvert;\n  }\n  {\n    assertProp(prop, key, value, vm, absent);\n  }\n  return value\n}\n\n/**\n * Get the default value of a prop.\n */\nfunction getPropDefaultValue (vm, prop, key) {\n  // no default, return undefined\n  if (!hasOwn(prop, 'default')) {\n    return undefined\n  }\n  var def = prop.default;\n  // warn against non-factory defaults for Object & Array\n  if (\"development\" !== 'production' && isObject(def)) {\n    warn(\n      'Invalid default value for prop \"' + key + '\": ' +\n      'Props with type Object/Array must use a factory function ' +\n      'to return the default value.',\n      vm\n    );\n  }\n  // the raw prop value was also undefined from previous render,\n  // return previous default value to avoid unnecessary watcher trigger\n  if (vm && vm.$options.propsData &&\n    vm.$options.propsData[key] === undefined &&\n    vm._props[key] !== undefined\n  ) {\n    return vm._props[key]\n  }\n  // call factory function for non-Function types\n  // a value is Function if its prototype is function even across different execution context\n  return typeof def === 'function' && getType(prop.type) !== 'Function'\n    ? def.call(vm)\n    : def\n}\n\n/**\n * Assert whether a prop is valid.\n */\nfunction assertProp (\n  prop,\n  name,\n  value,\n  vm,\n  absent\n) {\n  if (prop.required && absent) {\n    warn(\n      'Missing required prop: \"' + name + '\"',\n      vm\n    );\n    return\n  }\n  if (value == null && !prop.required) {\n    return\n  }\n  var type = prop.type;\n  var valid = !type || type === true;\n  var expectedTypes = [];\n  if (type) {\n    if (!Array.isArray(type)) {\n      type = [type];\n    }\n    for (var i = 0; i < type.length && !valid; i++) {\n      var assertedType = assertType(value, type[i]);\n      expectedTypes.push(assertedType.expectedType || '');\n      valid = assertedType.valid;\n    }\n  }\n  if (!valid) {\n    warn(\n      \"Invalid prop: type check failed for prop \\\"\" + name + \"\\\".\" +\n      \" Expected \" + (expectedTypes.map(capitalize).join(', ')) +\n      \", got \" + (toRawType(value)) + \".\",\n      vm\n    );\n    return\n  }\n  var validator = prop.validator;\n  if (validator) {\n    if (!validator(value)) {\n      warn(\n        'Invalid prop: custom validator check failed for prop \"' + name + '\".',\n        vm\n      );\n    }\n  }\n}\n\nvar simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/;\n\nfunction assertType (value, type) {\n  var valid;\n  var expectedType = getType(type);\n  if (simpleCheckRE.test(expectedType)) {\n    var t = typeof value;\n    valid = t === expectedType.toLowerCase();\n    // for primitive wrapper objects\n    if (!valid && t === 'object') {\n      valid = value instanceof type;\n    }\n  } else if (expectedType === 'Object') {\n    valid = isPlainObject(value);\n  } else if (expectedType === 'Array') {\n    valid = Array.isArray(value);\n  } else {\n    valid = value instanceof type;\n  }\n  return {\n    valid: valid,\n    expectedType: expectedType\n  }\n}\n\n/**\n * Use function string name to check built-in types,\n * because a simple equality check will fail when running\n * across different vms / iframes.\n */\nfunction getType (fn) {\n  var match = fn && fn.toString().match(/^\\s*function (\\w+)/);\n  return match ? match[1] : ''\n}\n\nfunction isType (type, fn) {\n  if (!Array.isArray(fn)) {\n    return getType(fn) === getType(type)\n  }\n  for (var i = 0, len = fn.length; i < len; i++) {\n    if (getType(fn[i]) === getType(type)) {\n      return true\n    }\n  }\n  /* istanbul ignore next */\n  return false\n}\n\n/*  */\n\nfunction handleError (err, vm, info) {\n  if (vm) {\n    var cur = vm;\n    while ((cur = cur.$parent)) {\n      var hooks = cur.$options.errorCaptured;\n      if (hooks) {\n        for (var i = 0; i < hooks.length; i++) {\n          try {\n            var capture = hooks[i].call(cur, err, vm, info) === false;\n            if (capture) { return }\n          } catch (e) {\n            globalHandleError(e, cur, 'errorCaptured hook');\n          }\n        }\n      }\n    }\n  }\n  globalHandleError(err, vm, info);\n}\n\nfunction globalHandleError (err, vm, info) {\n  if (config.errorHandler) {\n    try {\n      return config.errorHandler.call(null, err, vm, info)\n    } catch (e) {\n      logError(e, null, 'config.errorHandler');\n    }\n  }\n  logError(err, vm, info);\n}\n\nfunction logError (err, vm, info) {\n  {\n    warn((\"Error in \" + info + \": \\\"\" + (err.toString()) + \"\\\"\"), vm);\n  }\n  /* istanbul ignore else */\n  if ((inBrowser || inWeex) && typeof console !== 'undefined') {\n    console.error(err);\n  } else {\n    throw err\n  }\n}\n\n/*  */\n/* globals MessageChannel */\n\nvar callbacks = [];\nvar pending = false;\n\nfunction flushCallbacks () {\n  pending = false;\n  var copies = callbacks.slice(0);\n  callbacks.length = 0;\n  for (var i = 0; i < copies.length; i++) {\n    copies[i]();\n  }\n}\n\n// Here we have async deferring wrappers using both micro and macro tasks.\n// In < 2.4 we used micro tasks everywhere, but there are some scenarios where\n// micro tasks have too high a priority and fires in between supposedly\n// sequential events (e.g. #4521, #6690) or even between bubbling of the same\n// event (#6566). However, using macro tasks everywhere also has subtle problems\n// when state is changed right before repaint (e.g. #6813, out-in transitions).\n// Here we use micro task by default, but expose a way to force macro task when\n// needed (e.g. in event handlers attached by v-on).\nvar microTimerFunc;\nvar macroTimerFunc;\nvar useMacroTask = false;\n\n// Determine (macro) Task defer implementation.\n// Technically setImmediate should be the ideal choice, but it's only available\n// in IE. The only polyfill that consistently queues the callback after all DOM\n// events triggered in the same loop is by using MessageChannel.\n/* istanbul ignore if */\nif (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {\n  macroTimerFunc = function () {\n    setImmediate(flushCallbacks);\n  };\n} else if (typeof MessageChannel !== 'undefined' && (\n  isNative(MessageChannel) ||\n  // PhantomJS\n  MessageChannel.toString() === '[object MessageChannelConstructor]'\n)) {\n  var channel = new MessageChannel();\n  var port = channel.port2;\n  channel.port1.onmessage = flushCallbacks;\n  macroTimerFunc = function () {\n    port.postMessage(1);\n  };\n} else {\n  /* istanbul ignore next */\n  macroTimerFunc = function () {\n    setTimeout(flushCallbacks, 0);\n  };\n}\n\n// Determine MicroTask defer implementation.\n/* istanbul ignore next, $flow-disable-line */\nif (typeof Promise !== 'undefined' && isNative(Promise)) {\n  var p = Promise.resolve();\n  microTimerFunc = function () {\n    p.then(flushCallbacks);\n    // in problematic UIWebViews, Promise.then doesn't completely break, but\n    // it can get stuck in a weird state where callbacks are pushed into the\n    // microtask queue but the queue isn't being flushed, until the browser\n    // needs to do some other work, e.g. handle a timer. Therefore we can\n    // \"force\" the microtask queue to be flushed by adding an empty timer.\n    if (isIOS) { setTimeout(noop); }\n  };\n} else {\n  // fallback to macro\n  microTimerFunc = macroTimerFunc;\n}\n\n/**\n * Wrap a function so that if any code inside triggers state change,\n * the changes are queued using a Task instead of a MicroTask.\n */\nfunction withMacroTask (fn) {\n  return fn._withTask || (fn._withTask = function () {\n    useMacroTask = true;\n    var res = fn.apply(null, arguments);\n    useMacroTask = false;\n    return res\n  })\n}\n\nfunction nextTick (cb, ctx) {\n  var _resolve;\n  callbacks.push(function () {\n    if (cb) {\n      try {\n        cb.call(ctx);\n      } catch (e) {\n        handleError(e, ctx, 'nextTick');\n      }\n    } else if (_resolve) {\n      _resolve(ctx);\n    }\n  });\n  if (!pending) {\n    pending = true;\n    if (useMacroTask) {\n      macroTimerFunc();\n    } else {\n      microTimerFunc();\n    }\n  }\n  // $flow-disable-line\n  if (!cb && typeof Promise !== 'undefined') {\n    return new Promise(function (resolve) {\n      _resolve = resolve;\n    })\n  }\n}\n\n/*  */\n\nvar mark;\nvar measure;\n\n{\n  var perf = inBrowser && window.performance;\n  /* istanbul ignore if */\n  if (\n    perf &&\n    perf.mark &&\n    perf.measure &&\n    perf.clearMarks &&\n    perf.clearMeasures\n  ) {\n    mark = function (tag) { return perf.mark(tag); };\n    measure = function (name, startTag, endTag) {\n      perf.measure(name, startTag, endTag);\n      perf.clearMarks(startTag);\n      perf.clearMarks(endTag);\n      perf.clearMeasures(name);\n    };\n  }\n}\n\n/* not type checking this file because flow doesn't play well with Proxy */\n\nvar initProxy;\n\n{\n  var allowedGlobals = makeMap(\n    'Infinity,undefined,NaN,isFinite,isNaN,' +\n    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +\n    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +\n    'require' // for Webpack/Browserify\n  );\n\n  var warnNonPresent = function (target, key) {\n    warn(\n      \"Property or method \\\"\" + key + \"\\\" is not defined on the instance but \" +\n      'referenced during render. Make sure that this property is reactive, ' +\n      'either in the data option, or for class-based components, by ' +\n      'initializing the property. ' +\n      'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',\n      target\n    );\n  };\n\n  var hasProxy =\n    typeof Proxy !== 'undefined' &&\n    Proxy.toString().match(/native code/);\n\n  if (hasProxy) {\n    var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact');\n    config.keyCodes = new Proxy(config.keyCodes, {\n      set: function set (target, key, value) {\n        if (isBuiltInModifier(key)) {\n          warn((\"Avoid overwriting built-in modifier in config.keyCodes: .\" + key));\n          return false\n        } else {\n          target[key] = value;\n          return true\n        }\n      }\n    });\n  }\n\n  var hasHandler = {\n    has: function has (target, key) {\n      var has = key in target;\n      var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';\n      if (!has && !isAllowed) {\n        warnNonPresent(target, key);\n      }\n      return has || !isAllowed\n    }\n  };\n\n  var getHandler = {\n    get: function get (target, key) {\n      if (typeof key === 'string' && !(key in target)) {\n        warnNonPresent(target, key);\n      }\n      return target[key]\n    }\n  };\n\n  initProxy = function initProxy (vm) {\n    if (hasProxy) {\n      // determine which proxy handler to use\n      var options = vm.$options;\n      var handlers = options.render && options.render._withStripped\n        ? getHandler\n        : hasHandler;\n      vm._renderProxy = new Proxy(vm, handlers);\n    } else {\n      vm._renderProxy = vm;\n    }\n  };\n}\n\n/*  */\n\nvar seenObjects = new _Set();\n\n/**\n * Recursively traverse an object to evoke all converted\n * getters, so that every nested property inside the object\n * is collected as a \"deep\" dependency.\n */\nfunction traverse (val) {\n  _traverse(val, seenObjects);\n  seenObjects.clear();\n}\n\nfunction _traverse (val, seen) {\n  var i, keys;\n  var isA = Array.isArray(val);\n  if ((!isA && !isObject(val)) || Object.isFrozen(val)) {\n    return\n  }\n  if (val.__ob__) {\n    var depId = val.__ob__.dep.id;\n    if (seen.has(depId)) {\n      return\n    }\n    seen.add(depId);\n  }\n  if (isA) {\n    i = val.length;\n    while (i--) { _traverse(val[i], seen); }\n  } else {\n    keys = Object.keys(val);\n    i = keys.length;\n    while (i--) { _traverse(val[keys[i]], seen); }\n  }\n}\n\n/*  */\n\nvar normalizeEvent = cached(function (name) {\n  var passive = name.charAt(0) === '&';\n  name = passive ? name.slice(1) : name;\n  var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first\n  name = once$$1 ? name.slice(1) : name;\n  var capture = name.charAt(0) === '!';\n  name = capture ? name.slice(1) : name;\n  return {\n    name: name,\n    once: once$$1,\n    capture: capture,\n    passive: passive\n  }\n});\n\nfunction createFnInvoker (fns) {\n  function invoker () {\n    var arguments$1 = arguments;\n\n    var fns = invoker.fns;\n    if (Array.isArray(fns)) {\n      var cloned = fns.slice();\n      for (var i = 0; i < cloned.length; i++) {\n        cloned[i].apply(null, arguments$1);\n      }\n    } else {\n      // return handler return value for single handlers\n      return fns.apply(null, arguments)\n    }\n  }\n  invoker.fns = fns;\n  return invoker\n}\n\nfunction updateListeners (\n  on,\n  oldOn,\n  add,\n  remove$$1,\n  vm\n) {\n  var name, cur, old, event;\n  for (name in on) {\n    cur = on[name];\n    old = oldOn[name];\n    event = normalizeEvent(name);\n    if (isUndef(cur)) {\n      \"development\" !== 'production' && warn(\n        \"Invalid handler for event \\\"\" + (event.name) + \"\\\": got \" + String(cur),\n        vm\n      );\n    } else if (isUndef(old)) {\n      if (isUndef(cur.fns)) {\n        cur = on[name] = createFnInvoker(cur);\n      }\n      add(event.name, cur, event.once, event.capture, event.passive);\n    } else if (cur !== old) {\n      old.fns = cur;\n      on[name] = old;\n    }\n  }\n  for (name in oldOn) {\n    if (isUndef(on[name])) {\n      event = normalizeEvent(name);\n      remove$$1(event.name, oldOn[name], event.capture);\n    }\n  }\n}\n\n/*  */\n\nfunction mergeVNodeHook (def, hookKey, hook) {\n  if (def instanceof VNode) {\n    def = def.data.hook || (def.data.hook = {});\n  }\n  var invoker;\n  var oldHook = def[hookKey];\n\n  function wrappedHook () {\n    hook.apply(this, arguments);\n    // important: remove merged hook to ensure it's called only once\n    // and prevent memory leak\n    remove(invoker.fns, wrappedHook);\n  }\n\n  if (isUndef(oldHook)) {\n    // no existing hook\n    invoker = createFnInvoker([wrappedHook]);\n  } else {\n    /* istanbul ignore if */\n    if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {\n      // already a merged invoker\n      invoker = oldHook;\n      invoker.fns.push(wrappedHook);\n    } else {\n      // existing plain hook\n      invoker = createFnInvoker([oldHook, wrappedHook]);\n    }\n  }\n\n  invoker.merged = true;\n  def[hookKey] = invoker;\n}\n\n/*  */\n\nfunction extractPropsFromVNodeData (\n  data,\n  Ctor,\n  tag\n) {\n  // we are only extracting raw values here.\n  // validation and default values are handled in the child\n  // component itself.\n  var propOptions = Ctor.options.props;\n  if (isUndef(propOptions)) {\n    return\n  }\n  var res = {};\n  var attrs = data.attrs;\n  var props = data.props;\n  if (isDef(attrs) || isDef(props)) {\n    for (var key in propOptions) {\n      var altKey = hyphenate(key);\n      {\n        var keyInLowerCase = key.toLowerCase();\n        if (\n          key !== keyInLowerCase &&\n          attrs && hasOwn(attrs, keyInLowerCase)\n        ) {\n          tip(\n            \"Prop \\\"\" + keyInLowerCase + \"\\\" is passed to component \" +\n            (formatComponentName(tag || Ctor)) + \", but the declared prop name is\" +\n            \" \\\"\" + key + \"\\\". \" +\n            \"Note that HTML attributes are case-insensitive and camelCased \" +\n            \"props need to use their kebab-case equivalents when using in-DOM \" +\n            \"templates. You should probably use \\\"\" + altKey + \"\\\" instead of \\\"\" + key + \"\\\".\"\n          );\n        }\n      }\n      checkProp(res, props, key, altKey, true) ||\n      checkProp(res, attrs, key, altKey, false);\n    }\n  }\n  return res\n}\n\nfunction checkProp (\n  res,\n  hash,\n  key,\n  altKey,\n  preserve\n) {\n  if (isDef(hash)) {\n    if (hasOwn(hash, key)) {\n      res[key] = hash[key];\n      if (!preserve) {\n        delete hash[key];\n      }\n      return true\n    } else if (hasOwn(hash, altKey)) {\n      res[key] = hash[altKey];\n      if (!preserve) {\n        delete hash[altKey];\n      }\n      return true\n    }\n  }\n  return false\n}\n\n/*  */\n\n// The template compiler attempts to minimize the need for normalization by\n// statically analyzing the template at compile time.\n//\n// For plain HTML markup, normalization can be completely skipped because the\n// generated render function is guaranteed to return Array<VNode>. There are\n// two cases where extra normalization is needed:\n\n// 1. When the children contains components - because a functional component\n// may return an Array instead of a single root. In this case, just a simple\n// normalization is needed - if any child is an Array, we flatten the whole\n// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep\n// because functional components already normalize their own children.\nfunction simpleNormalizeChildren (children) {\n  for (var i = 0; i < children.length; i++) {\n    if (Array.isArray(children[i])) {\n      return Array.prototype.concat.apply([], children)\n    }\n  }\n  return children\n}\n\n// 2. When the children contains constructs that always generated nested Arrays,\n// e.g. <template>, <slot>, v-for, or when the children is provided by user\n// with hand-written render functions / JSX. In such cases a full normalization\n// is needed to cater to all possible types of children values.\nfunction normalizeChildren (children) {\n  return isPrimitive(children)\n    ? [createTextVNode(children)]\n    : Array.isArray(children)\n      ? normalizeArrayChildren(children)\n      : undefined\n}\n\nfunction isTextNode (node) {\n  return isDef(node) && isDef(node.text) && isFalse(node.isComment)\n}\n\nfunction normalizeArrayChildren (children, nestedIndex) {\n  var res = [];\n  var i, c, lastIndex, last;\n  for (i = 0; i < children.length; i++) {\n    c = children[i];\n    if (isUndef(c) || typeof c === 'boolean') { continue }\n    lastIndex = res.length - 1;\n    last = res[lastIndex];\n    //  nested\n    if (Array.isArray(c)) {\n      if (c.length > 0) {\n        c = normalizeArrayChildren(c, ((nestedIndex || '') + \"_\" + i));\n        // merge adjacent text nodes\n        if (isTextNode(c[0]) && isTextNode(last)) {\n          res[lastIndex] = createTextVNode(last.text + (c[0]).text);\n          c.shift();\n        }\n        res.push.apply(res, c);\n      }\n    } else if (isPrimitive(c)) {\n      if (isTextNode(last)) {\n        // merge adjacent text nodes\n        // this is necessary for SSR hydration because text nodes are\n        // essentially merged when rendered to HTML strings\n        res[lastIndex] = createTextVNode(last.text + c);\n      } else if (c !== '') {\n        // convert primitive to vnode\n        res.push(createTextVNode(c));\n      }\n    } else {\n      if (isTextNode(c) && isTextNode(last)) {\n        // merge adjacent text nodes\n        res[lastIndex] = createTextVNode(last.text + c.text);\n      } else {\n        // default key for nested array children (likely generated by v-for)\n        if (isTrue(children._isVList) &&\n          isDef(c.tag) &&\n          isUndef(c.key) &&\n          isDef(nestedIndex)) {\n          c.key = \"__vlist\" + nestedIndex + \"_\" + i + \"__\";\n        }\n        res.push(c);\n      }\n    }\n  }\n  return res\n}\n\n/*  */\n\nfunction ensureCtor (comp, base) {\n  if (\n    comp.__esModule ||\n    (hasSymbol && comp[Symbol.toStringTag] === 'Module')\n  ) {\n    comp = comp.default;\n  }\n  return isObject(comp)\n    ? base.extend(comp)\n    : comp\n}\n\nfunction createAsyncPlaceholder (\n  factory,\n  data,\n  context,\n  children,\n  tag\n) {\n  var node = createEmptyVNode();\n  node.asyncFactory = factory;\n  node.asyncMeta = { data: data, context: context, children: children, tag: tag };\n  return node\n}\n\nfunction resolveAsyncComponent (\n  factory,\n  baseCtor,\n  context\n) {\n  if (isTrue(factory.error) && isDef(factory.errorComp)) {\n    return factory.errorComp\n  }\n\n  if (isDef(factory.resolved)) {\n    return factory.resolved\n  }\n\n  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {\n    return factory.loadingComp\n  }\n\n  if (isDef(factory.contexts)) {\n    // already pending\n    factory.contexts.push(context);\n  } else {\n    var contexts = factory.contexts = [context];\n    var sync = true;\n\n    var forceRender = function () {\n      for (var i = 0, l = contexts.length; i < l; i++) {\n        contexts[i].$forceUpdate();\n      }\n    };\n\n    var resolve = once(function (res) {\n      // cache resolved\n      factory.resolved = ensureCtor(res, baseCtor);\n      // invoke callbacks only if this is not a synchronous resolve\n      // (async resolves are shimmed as synchronous during SSR)\n      if (!sync) {\n        forceRender();\n      }\n    });\n\n    var reject = once(function (reason) {\n      \"development\" !== 'production' && warn(\n        \"Failed to resolve async component: \" + (String(factory)) +\n        (reason ? (\"\\nReason: \" + reason) : '')\n      );\n      if (isDef(factory.errorComp)) {\n        factory.error = true;\n        forceRender();\n      }\n    });\n\n    var res = factory(resolve, reject);\n\n    if (isObject(res)) {\n      if (typeof res.then === 'function') {\n        // () => Promise\n        if (isUndef(factory.resolved)) {\n          res.then(resolve, reject);\n        }\n      } else if (isDef(res.component) && typeof res.component.then === 'function') {\n        res.component.then(resolve, reject);\n\n        if (isDef(res.error)) {\n          factory.errorComp = ensureCtor(res.error, baseCtor);\n        }\n\n        if (isDef(res.loading)) {\n          factory.loadingComp = ensureCtor(res.loading, baseCtor);\n          if (res.delay === 0) {\n            factory.loading = true;\n          } else {\n            setTimeout(function () {\n              if (isUndef(factory.resolved) && isUndef(factory.error)) {\n                factory.loading = true;\n                forceRender();\n              }\n            }, res.delay || 200);\n          }\n        }\n\n        if (isDef(res.timeout)) {\n          setTimeout(function () {\n            if (isUndef(factory.resolved)) {\n              reject(\n                \"timeout (\" + (res.timeout) + \"ms)\"\n              );\n            }\n          }, res.timeout);\n        }\n      }\n    }\n\n    sync = false;\n    // return in case resolved synchronously\n    return factory.loading\n      ? factory.loadingComp\n      : factory.resolved\n  }\n}\n\n/*  */\n\nfunction isAsyncPlaceholder (node) {\n  return node.isComment && node.asyncFactory\n}\n\n/*  */\n\nfunction getFirstComponentChild (children) {\n  if (Array.isArray(children)) {\n    for (var i = 0; i < children.length; i++) {\n      var c = children[i];\n      if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {\n        return c\n      }\n    }\n  }\n}\n\n/*  */\n\n/*  */\n\nfunction initEvents (vm) {\n  vm._events = Object.create(null);\n  vm._hasHookEvent = false;\n  // init parent attached events\n  var listeners = vm.$options._parentListeners;\n  if (listeners) {\n    updateComponentListeners(vm, listeners);\n  }\n}\n\nvar target;\n\nfunction add (event, fn, once) {\n  if (once) {\n    target.$once(event, fn);\n  } else {\n    target.$on(event, fn);\n  }\n}\n\nfunction remove$1 (event, fn) {\n  target.$off(event, fn);\n}\n\nfunction updateComponentListeners (\n  vm,\n  listeners,\n  oldListeners\n) {\n  target = vm;\n  updateListeners(listeners, oldListeners || {}, add, remove$1, vm);\n  target = undefined;\n}\n\nfunction eventsMixin (Vue) {\n  var hookRE = /^hook:/;\n  Vue.prototype.$on = function (event, fn) {\n    var this$1 = this;\n\n    var vm = this;\n    if (Array.isArray(event)) {\n      for (var i = 0, l = event.length; i < l; i++) {\n        this$1.$on(event[i], fn);\n      }\n    } else {\n      (vm._events[event] || (vm._events[event] = [])).push(fn);\n      // optimize hook:event cost by using a boolean flag marked at registration\n      // instead of a hash lookup\n      if (hookRE.test(event)) {\n        vm._hasHookEvent = true;\n      }\n    }\n    return vm\n  };\n\n  Vue.prototype.$once = function (event, fn) {\n    var vm = this;\n    function on () {\n      vm.$off(event, on);\n      fn.apply(vm, arguments);\n    }\n    on.fn = fn;\n    vm.$on(event, on);\n    return vm\n  };\n\n  Vue.prototype.$off = function (event, fn) {\n    var this$1 = this;\n\n    var vm = this;\n    // all\n    if (!arguments.length) {\n      vm._events = Object.create(null);\n      return vm\n    }\n    // array of events\n    if (Array.isArray(event)) {\n      for (var i = 0, l = event.length; i < l; i++) {\n        this$1.$off(event[i], fn);\n      }\n      return vm\n    }\n    // specific event\n    var cbs = vm._events[event];\n    if (!cbs) {\n      return vm\n    }\n    if (!fn) {\n      vm._events[event] = null;\n      return vm\n    }\n    if (fn) {\n      // specific handler\n      var cb;\n      var i$1 = cbs.length;\n      while (i$1--) {\n        cb = cbs[i$1];\n        if (cb === fn || cb.fn === fn) {\n          cbs.splice(i$1, 1);\n          break\n        }\n      }\n    }\n    return vm\n  };\n\n  Vue.prototype.$emit = function (event) {\n    var vm = this;\n    {\n      var lowerCaseEvent = event.toLowerCase();\n      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {\n        tip(\n          \"Event \\\"\" + lowerCaseEvent + \"\\\" is emitted in component \" +\n          (formatComponentName(vm)) + \" but the handler is registered for \\\"\" + event + \"\\\". \" +\n          \"Note that HTML attributes are case-insensitive and you cannot use \" +\n          \"v-on to listen to camelCase events when using in-DOM templates. \" +\n          \"You should probably use \\\"\" + (hyphenate(event)) + \"\\\" instead of \\\"\" + event + \"\\\".\"\n        );\n      }\n    }\n    var cbs = vm._events[event];\n    if (cbs) {\n      cbs = cbs.length > 1 ? toArray(cbs) : cbs;\n      var args = toArray(arguments, 1);\n      for (var i = 0, l = cbs.length; i < l; i++) {\n        try {\n          cbs[i].apply(vm, args);\n        } catch (e) {\n          handleError(e, vm, (\"event handler for \\\"\" + event + \"\\\"\"));\n        }\n      }\n    }\n    return vm\n  };\n}\n\n/*  */\n\n/**\n * Runtime helper for resolving raw children VNodes into a slot object.\n */\nfunction resolveSlots (\n  children,\n  context\n) {\n  var slots = {};\n  if (!children) {\n    return slots\n  }\n  for (var i = 0, l = children.length; i < l; i++) {\n    var child = children[i];\n    var data = child.data;\n    // remove slot attribute if the node is resolved as a Vue slot node\n    if (data && data.attrs && data.attrs.slot) {\n      delete data.attrs.slot;\n    }\n    // named slots should only be respected if the vnode was rendered in the\n    // same context.\n    if ((child.context === context || child.fnContext === context) &&\n      data && data.slot != null\n    ) {\n      var name = child.data.slot;\n      var slot = (slots[name] || (slots[name] = []));\n      if (child.tag === 'template') {\n        slot.push.apply(slot, child.children);\n      } else {\n        slot.push(child);\n      }\n    } else {\n      (slots.default || (slots.default = [])).push(child);\n    }\n  }\n  // ignore slots that contains only whitespace\n  for (var name$1 in slots) {\n    if (slots[name$1].every(isWhitespace)) {\n      delete slots[name$1];\n    }\n  }\n  return slots\n}\n\nfunction isWhitespace (node) {\n  return (node.isComment && !node.asyncFactory) || node.text === ' '\n}\n\nfunction resolveScopedSlots (\n  fns, // see flow/vnode\n  res\n) {\n  res = res || {};\n  for (var i = 0; i < fns.length; i++) {\n    if (Array.isArray(fns[i])) {\n      resolveScopedSlots(fns[i], res);\n    } else {\n      res[fns[i].key] = fns[i].fn;\n    }\n  }\n  return res\n}\n\n/*  */\n\nvar activeInstance = null;\nvar isUpdatingChildComponent = false;\n\nfunction initLifecycle (vm) {\n  var options = vm.$options;\n\n  // locate first non-abstract parent\n  var parent = options.parent;\n  if (parent && !options.abstract) {\n    while (parent.$options.abstract && parent.$parent) {\n      parent = parent.$parent;\n    }\n    parent.$children.push(vm);\n  }\n\n  vm.$parent = parent;\n  vm.$root = parent ? parent.$root : vm;\n\n  vm.$children = [];\n  vm.$refs = {};\n\n  vm._watcher = null;\n  vm._inactive = null;\n  vm._directInactive = false;\n  vm._isMounted = false;\n  vm._isDestroyed = false;\n  vm._isBeingDestroyed = false;\n}\n\nfunction lifecycleMixin (Vue) {\n  Vue.prototype._update = function (vnode, hydrating) {\n    var vm = this;\n    if (vm._isMounted) {\n      callHook(vm, 'beforeUpdate');\n    }\n    var prevEl = vm.$el;\n    var prevVnode = vm._vnode;\n    var prevActiveInstance = activeInstance;\n    activeInstance = vm;\n    vm._vnode = vnode;\n    // Vue.prototype.__patch__ is injected in entry points\n    // based on the rendering backend used.\n    if (!prevVnode) {\n      // initial render\n      vm.$el = vm.__patch__(\n        vm.$el, vnode, hydrating, false /* removeOnly */,\n        vm.$options._parentElm,\n        vm.$options._refElm\n      );\n      // no need for the ref nodes after initial patch\n      // this prevents keeping a detached DOM tree in memory (#5851)\n      vm.$options._parentElm = vm.$options._refElm = null;\n    } else {\n      // updates\n      vm.$el = vm.__patch__(prevVnode, vnode);\n    }\n    activeInstance = prevActiveInstance;\n    // update __vue__ reference\n    if (prevEl) {\n      prevEl.__vue__ = null;\n    }\n    if (vm.$el) {\n      vm.$el.__vue__ = vm;\n    }\n    // if parent is an HOC, update its $el as well\n    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {\n      vm.$parent.$el = vm.$el;\n    }\n    // updated hook is called by the scheduler to ensure that children are\n    // updated in a parent's updated hook.\n  };\n\n  Vue.prototype.$forceUpdate = function () {\n    var vm = this;\n    if (vm._watcher) {\n      vm._watcher.update();\n    }\n  };\n\n  Vue.prototype.$destroy = function () {\n    var vm = this;\n    if (vm._isBeingDestroyed) {\n      return\n    }\n    callHook(vm, 'beforeDestroy');\n    vm._isBeingDestroyed = true;\n    // remove self from parent\n    var parent = vm.$parent;\n    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {\n      remove(parent.$children, vm);\n    }\n    // teardown watchers\n    if (vm._watcher) {\n      vm._watcher.teardown();\n    }\n    var i = vm._watchers.length;\n    while (i--) {\n      vm._watchers[i].teardown();\n    }\n    // remove reference from data ob\n    // frozen object may not have observer.\n    if (vm._data.__ob__) {\n      vm._data.__ob__.vmCount--;\n    }\n    // call the last hook...\n    vm._isDestroyed = true;\n    // invoke destroy hooks on current rendered tree\n    vm.__patch__(vm._vnode, null);\n    // fire destroyed hook\n    callHook(vm, 'destroyed');\n    // turn off all instance listeners.\n    vm.$off();\n    // remove __vue__ reference\n    if (vm.$el) {\n      vm.$el.__vue__ = null;\n    }\n    // release circular reference (#6759)\n    if (vm.$vnode) {\n      vm.$vnode.parent = null;\n    }\n  };\n}\n\nfunction mountComponent (\n  vm,\n  el,\n  hydrating\n) {\n  vm.$el = el;\n  if (!vm.$options.render) {\n    vm.$options.render = createEmptyVNode;\n    {\n      /* istanbul ignore if */\n      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||\n        vm.$options.el || el) {\n        warn(\n          'You are using the runtime-only build of Vue where the template ' +\n          'compiler is not available. Either pre-compile the templates into ' +\n          'render functions, or use the compiler-included build.',\n          vm\n        );\n      } else {\n        warn(\n          'Failed to mount component: template or render function not defined.',\n          vm\n        );\n      }\n    }\n  }\n  callHook(vm, 'beforeMount');\n\n  var updateComponent;\n  /* istanbul ignore if */\n  if (\"development\" !== 'production' && config.performance && mark) {\n    updateComponent = function () {\n      var name = vm._name;\n      var id = vm._uid;\n      var startTag = \"vue-perf-start:\" + id;\n      var endTag = \"vue-perf-end:\" + id;\n\n      mark(startTag);\n      var vnode = vm._render();\n      mark(endTag);\n      measure((\"vue \" + name + \" render\"), startTag, endTag);\n\n      mark(startTag);\n      vm._update(vnode, hydrating);\n      mark(endTag);\n      measure((\"vue \" + name + \" patch\"), startTag, endTag);\n    };\n  } else {\n    updateComponent = function () {\n      vm._update(vm._render(), hydrating);\n    };\n  }\n\n  // we set this to vm._watcher inside the watcher's constructor\n  // since the watcher's initial patch may call $forceUpdate (e.g. inside child\n  // component's mounted hook), which relies on vm._watcher being already defined\n  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);\n  hydrating = false;\n\n  // manually mounted instance, call mounted on self\n  // mounted is called for render-created child components in its inserted hook\n  if (vm.$vnode == null) {\n    vm._isMounted = true;\n    callHook(vm, 'mounted');\n  }\n  return vm\n}\n\nfunction updateChildComponent (\n  vm,\n  propsData,\n  listeners,\n  parentVnode,\n  renderChildren\n) {\n  {\n    isUpdatingChildComponent = true;\n  }\n\n  // determine whether component has slot children\n  // we need to do this before overwriting $options._renderChildren\n  var hasChildren = !!(\n    renderChildren ||               // has new static slots\n    vm.$options._renderChildren ||  // has old static slots\n    parentVnode.data.scopedSlots || // has new scoped slots\n    vm.$scopedSlots !== emptyObject // has old scoped slots\n  );\n\n  vm.$options._parentVnode = parentVnode;\n  vm.$vnode = parentVnode; // update vm's placeholder node without re-render\n\n  if (vm._vnode) { // update child tree's parent\n    vm._vnode.parent = parentVnode;\n  }\n  vm.$options._renderChildren = renderChildren;\n\n  // update $attrs and $listeners hash\n  // these are also reactive so they may trigger child update if the child\n  // used them during render\n  vm.$attrs = (parentVnode.data && parentVnode.data.attrs) || emptyObject;\n  vm.$listeners = listeners || emptyObject;\n\n  // update props\n  if (propsData && vm.$options.props) {\n    observerState.shouldConvert = false;\n    var props = vm._props;\n    var propKeys = vm.$options._propKeys || [];\n    for (var i = 0; i < propKeys.length; i++) {\n      var key = propKeys[i];\n      props[key] = validateProp(key, vm.$options.props, propsData, vm);\n    }\n    observerState.shouldConvert = true;\n    // keep a copy of raw propsData\n    vm.$options.propsData = propsData;\n  }\n\n  // update listeners\n  if (listeners) {\n    var oldListeners = vm.$options._parentListeners;\n    vm.$options._parentListeners = listeners;\n    updateComponentListeners(vm, listeners, oldListeners);\n  }\n  // resolve slots + force update if has children\n  if (hasChildren) {\n    vm.$slots = resolveSlots(renderChildren, parentVnode.context);\n    vm.$forceUpdate();\n  }\n\n  {\n    isUpdatingChildComponent = false;\n  }\n}\n\nfunction isInInactiveTree (vm) {\n  while (vm && (vm = vm.$parent)) {\n    if (vm._inactive) { return true }\n  }\n  return false\n}\n\nfunction activateChildComponent (vm, direct) {\n  if (direct) {\n    vm._directInactive = false;\n    if (isInInactiveTree(vm)) {\n      return\n    }\n  } else if (vm._directInactive) {\n    return\n  }\n  if (vm._inactive || vm._inactive === null) {\n    vm._inactive = false;\n    for (var i = 0; i < vm.$children.length; i++) {\n      activateChildComponent(vm.$children[i]);\n    }\n    callHook(vm, 'activated');\n  }\n}\n\nfunction deactivateChildComponent (vm, direct) {\n  if (direct) {\n    vm._directInactive = true;\n    if (isInInactiveTree(vm)) {\n      return\n    }\n  }\n  if (!vm._inactive) {\n    vm._inactive = true;\n    for (var i = 0; i < vm.$children.length; i++) {\n      deactivateChildComponent(vm.$children[i]);\n    }\n    callHook(vm, 'deactivated');\n  }\n}\n\nfunction callHook (vm, hook) {\n  var handlers = vm.$options[hook];\n  if (handlers) {\n    for (var i = 0, j = handlers.length; i < j; i++) {\n      try {\n        handlers[i].call(vm);\n      } catch (e) {\n        handleError(e, vm, (hook + \" hook\"));\n      }\n    }\n  }\n  if (vm._hasHookEvent) {\n    vm.$emit('hook:' + hook);\n  }\n}\n\n/*  */\n\n\nvar MAX_UPDATE_COUNT = 100;\n\nvar queue = [];\nvar activatedChildren = [];\nvar has = {};\nvar circular = {};\nvar waiting = false;\nvar flushing = false;\nvar index = 0;\n\n/**\n * Reset the scheduler's state.\n */\nfunction resetSchedulerState () {\n  index = queue.length = activatedChildren.length = 0;\n  has = {};\n  {\n    circular = {};\n  }\n  waiting = flushing = false;\n}\n\n/**\n * Flush both queues and run the watchers.\n */\nfunction flushSchedulerQueue () {\n  flushing = true;\n  var watcher, id;\n\n  // Sort queue before flush.\n  // This ensures that:\n  // 1. Components are updated from parent to child. (because parent is always\n  //    created before the child)\n  // 2. A component's user watchers are run before its render watcher (because\n  //    user watchers are created before the render watcher)\n  // 3. If a component is destroyed during a parent component's watcher run,\n  //    its watchers can be skipped.\n  queue.sort(function (a, b) { return a.id - b.id; });\n\n  // do not cache length because more watchers might be pushed\n  // as we run existing watchers\n  for (index = 0; index < queue.length; index++) {\n    watcher = queue[index];\n    id = watcher.id;\n    has[id] = null;\n    watcher.run();\n    // in dev build, check and stop circular updates.\n    if (\"development\" !== 'production' && has[id] != null) {\n      circular[id] = (circular[id] || 0) + 1;\n      if (circular[id] > MAX_UPDATE_COUNT) {\n        warn(\n          'You may have an infinite update loop ' + (\n            watcher.user\n              ? (\"in watcher with expression \\\"\" + (watcher.expression) + \"\\\"\")\n              : \"in a component render function.\"\n          ),\n          watcher.vm\n        );\n        break\n      }\n    }\n  }\n\n  // keep copies of post queues before resetting state\n  var activatedQueue = activatedChildren.slice();\n  var updatedQueue = queue.slice();\n\n  resetSchedulerState();\n\n  // call component updated and activated hooks\n  callActivatedHooks(activatedQueue);\n  callUpdatedHooks(updatedQueue);\n\n  // devtool hook\n  /* istanbul ignore if */\n  if (devtools && config.devtools) {\n    devtools.emit('flush');\n  }\n}\n\nfunction callUpdatedHooks (queue) {\n  var i = queue.length;\n  while (i--) {\n    var watcher = queue[i];\n    var vm = watcher.vm;\n    if (vm._watcher === watcher && vm._isMounted) {\n      callHook(vm, 'updated');\n    }\n  }\n}\n\n/**\n * Queue a kept-alive component that was activated during patch.\n * The queue will be processed after the entire tree has been patched.\n */\nfunction queueActivatedComponent (vm) {\n  // setting _inactive to false here so that a render function can\n  // rely on checking whether it's in an inactive tree (e.g. router-view)\n  vm._inactive = false;\n  activatedChildren.push(vm);\n}\n\nfunction callActivatedHooks (queue) {\n  for (var i = 0; i < queue.length; i++) {\n    queue[i]._inactive = true;\n    activateChildComponent(queue[i], true /* true */);\n  }\n}\n\n/**\n * Push a watcher into the watcher queue.\n * Jobs with duplicate IDs will be skipped unless it's\n * pushed when the queue is being flushed.\n */\nfunction queueWatcher (watcher) {\n  var id = watcher.id;\n  if (has[id] == null) {\n    has[id] = true;\n    if (!flushing) {\n      queue.push(watcher);\n    } else {\n      // if already flushing, splice the watcher based on its id\n      // if already past its id, it will be run next immediately.\n      var i = queue.length - 1;\n      while (i > index && queue[i].id > watcher.id) {\n        i--;\n      }\n      queue.splice(i + 1, 0, watcher);\n    }\n    // queue the flush\n    if (!waiting) {\n      waiting = true;\n      nextTick(flushSchedulerQueue);\n    }\n  }\n}\n\n/*  */\n\nvar uid$2 = 0;\n\n/**\n * A watcher parses an expression, collects dependencies,\n * and fires callback when the expression value changes.\n * This is used for both the $watch() api and directives.\n */\nvar Watcher = function Watcher (\n  vm,\n  expOrFn,\n  cb,\n  options,\n  isRenderWatcher\n) {\n  this.vm = vm;\n  if (isRenderWatcher) {\n    vm._watcher = this;\n  }\n  vm._watchers.push(this);\n  // options\n  if (options) {\n    this.deep = !!options.deep;\n    this.user = !!options.user;\n    this.lazy = !!options.lazy;\n    this.sync = !!options.sync;\n  } else {\n    this.deep = this.user = this.lazy = this.sync = false;\n  }\n  this.cb = cb;\n  this.id = ++uid$2; // uid for batching\n  this.active = true;\n  this.dirty = this.lazy; // for lazy watchers\n  this.deps = [];\n  this.newDeps = [];\n  this.depIds = new _Set();\n  this.newDepIds = new _Set();\n  this.expression = expOrFn.toString();\n  // parse expression for getter\n  if (typeof expOrFn === 'function') {\n    this.getter = expOrFn;\n  } else {\n    this.getter = parsePath(expOrFn);\n    if (!this.getter) {\n      this.getter = function () {};\n      \"development\" !== 'production' && warn(\n        \"Failed watching path: \\\"\" + expOrFn + \"\\\" \" +\n        'Watcher only accepts simple dot-delimited paths. ' +\n        'For full control, use a function instead.',\n        vm\n      );\n    }\n  }\n  this.value = this.lazy\n    ? undefined\n    : this.get();\n};\n\n/**\n * Evaluate the getter, and re-collect dependencies.\n */\nWatcher.prototype.get = function get () {\n  pushTarget(this);\n  var value;\n  var vm = this.vm;\n  try {\n    value = this.getter.call(vm, vm);\n  } catch (e) {\n    if (this.user) {\n      handleError(e, vm, (\"getter for watcher \\\"\" + (this.expression) + \"\\\"\"));\n    } else {\n      throw e\n    }\n  } finally {\n    // \"touch\" every property so they are all tracked as\n    // dependencies for deep watching\n    if (this.deep) {\n      traverse(value);\n    }\n    popTarget();\n    this.cleanupDeps();\n  }\n  return value\n};\n\n/**\n * Add a dependency to this directive.\n */\nWatcher.prototype.addDep = function addDep (dep) {\n  var id = dep.id;\n  if (!this.newDepIds.has(id)) {\n    this.newDepIds.add(id);\n    this.newDeps.push(dep);\n    if (!this.depIds.has(id)) {\n      dep.addSub(this);\n    }\n  }\n};\n\n/**\n * Clean up for dependency collection.\n */\nWatcher.prototype.cleanupDeps = function cleanupDeps () {\n    var this$1 = this;\n\n  var i = this.deps.length;\n  while (i--) {\n    var dep = this$1.deps[i];\n    if (!this$1.newDepIds.has(dep.id)) {\n      dep.removeSub(this$1);\n    }\n  }\n  var tmp = this.depIds;\n  this.depIds = this.newDepIds;\n  this.newDepIds = tmp;\n  this.newDepIds.clear();\n  tmp = this.deps;\n  this.deps = this.newDeps;\n  this.newDeps = tmp;\n  this.newDeps.length = 0;\n};\n\n/**\n * Subscriber interface.\n * Will be called when a dependency changes.\n */\nWatcher.prototype.update = function update () {\n  /* istanbul ignore else */\n  if (this.lazy) {\n    this.dirty = true;\n  } else if (this.sync) {\n    this.run();\n  } else {\n    queueWatcher(this);\n  }\n};\n\n/**\n * Scheduler job interface.\n * Will be called by the scheduler.\n */\nWatcher.prototype.run = function run () {\n  if (this.active) {\n    var value = this.get();\n    if (\n      value !== this.value ||\n      // Deep watchers and watchers on Object/Arrays should fire even\n      // when the value is the same, because the value may\n      // have mutated.\n      isObject(value) ||\n      this.deep\n    ) {\n      // set new value\n      var oldValue = this.value;\n      this.value = value;\n      if (this.user) {\n        try {\n          this.cb.call(this.vm, value, oldValue);\n        } catch (e) {\n          handleError(e, this.vm, (\"callback for watcher \\\"\" + (this.expression) + \"\\\"\"));\n        }\n      } else {\n        this.cb.call(this.vm, value, oldValue);\n      }\n    }\n  }\n};\n\n/**\n * Evaluate the value of the watcher.\n * This only gets called for lazy watchers.\n */\nWatcher.prototype.evaluate = function evaluate () {\n  this.value = this.get();\n  this.dirty = false;\n};\n\n/**\n * Depend on all deps collected by this watcher.\n */\nWatcher.prototype.depend = function depend () {\n    var this$1 = this;\n\n  var i = this.deps.length;\n  while (i--) {\n    this$1.deps[i].depend();\n  }\n};\n\n/**\n * Remove self from all dependencies' subscriber list.\n */\nWatcher.prototype.teardown = function teardown () {\n    var this$1 = this;\n\n  if (this.active) {\n    // remove self from vm's watcher list\n    // this is a somewhat expensive operation so we skip it\n    // if the vm is being destroyed.\n    if (!this.vm._isBeingDestroyed) {\n      remove(this.vm._watchers, this);\n    }\n    var i = this.deps.length;\n    while (i--) {\n      this$1.deps[i].removeSub(this$1);\n    }\n    this.active = false;\n  }\n};\n\n/*  */\n\nvar sharedPropertyDefinition = {\n  enumerable: true,\n  configurable: true,\n  get: noop,\n  set: noop\n};\n\nfunction proxy (target, sourceKey, key) {\n  sharedPropertyDefinition.get = function proxyGetter () {\n    return this[sourceKey][key]\n  };\n  sharedPropertyDefinition.set = function proxySetter (val) {\n    this[sourceKey][key] = val;\n  };\n  Object.defineProperty(target, key, sharedPropertyDefinition);\n}\n\nfunction initState (vm) {\n  vm._watchers = [];\n  var opts = vm.$options;\n  if (opts.props) { initProps(vm, opts.props); }\n  if (opts.methods) { initMethods(vm, opts.methods); }\n  if (opts.data) {\n    initData(vm);\n  } else {\n    observe(vm._data = {}, true /* asRootData */);\n  }\n  if (opts.computed) { initComputed(vm, opts.computed); }\n  if (opts.watch && opts.watch !== nativeWatch) {\n    initWatch(vm, opts.watch);\n  }\n}\n\nfunction initProps (vm, propsOptions) {\n  var propsData = vm.$options.propsData || {};\n  var props = vm._props = {};\n  // cache prop keys so that future props updates can iterate using Array\n  // instead of dynamic object key enumeration.\n  var keys = vm.$options._propKeys = [];\n  var isRoot = !vm.$parent;\n  // root instance props should be converted\n  observerState.shouldConvert = isRoot;\n  var loop = function ( key ) {\n    keys.push(key);\n    var value = validateProp(key, propsOptions, propsData, vm);\n    /* istanbul ignore else */\n    {\n      var hyphenatedKey = hyphenate(key);\n      if (isReservedAttribute(hyphenatedKey) ||\n          config.isReservedAttr(hyphenatedKey)) {\n        warn(\n          (\"\\\"\" + hyphenatedKey + \"\\\" is a reserved attribute and cannot be used as component prop.\"),\n          vm\n        );\n      }\n      defineReactive(props, key, value, function () {\n        if (vm.$parent && !isUpdatingChildComponent) {\n          warn(\n            \"Avoid mutating a prop directly since the value will be \" +\n            \"overwritten whenever the parent component re-renders. \" +\n            \"Instead, use a data or computed property based on the prop's \" +\n            \"value. Prop being mutated: \\\"\" + key + \"\\\"\",\n            vm\n          );\n        }\n      });\n    }\n    // static props are already proxied on the component's prototype\n    // during Vue.extend(). We only need to proxy props defined at\n    // instantiation here.\n    if (!(key in vm)) {\n      proxy(vm, \"_props\", key);\n    }\n  };\n\n  for (var key in propsOptions) loop( key );\n  observerState.shouldConvert = true;\n}\n\nfunction initData (vm) {\n  var data = vm.$options.data;\n  data = vm._data = typeof data === 'function'\n    ? getData(data, vm)\n    : data || {};\n  if (!isPlainObject(data)) {\n    data = {};\n    \"development\" !== 'production' && warn(\n      'data functions should return an object:\\n' +\n      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',\n      vm\n    );\n  }\n  // proxy data on instance\n  var keys = Object.keys(data);\n  var props = vm.$options.props;\n  var methods = vm.$options.methods;\n  var i = keys.length;\n  while (i--) {\n    var key = keys[i];\n    {\n      if (methods && hasOwn(methods, key)) {\n        warn(\n          (\"Method \\\"\" + key + \"\\\" has already been defined as a data property.\"),\n          vm\n        );\n      }\n    }\n    if (props && hasOwn(props, key)) {\n      \"development\" !== 'production' && warn(\n        \"The data property \\\"\" + key + \"\\\" is already declared as a prop. \" +\n        \"Use prop default value instead.\",\n        vm\n      );\n    } else if (!isReserved(key)) {\n      proxy(vm, \"_data\", key);\n    }\n  }\n  // observe data\n  observe(data, true /* asRootData */);\n}\n\nfunction getData (data, vm) {\n  try {\n    return data.call(vm, vm)\n  } catch (e) {\n    handleError(e, vm, \"data()\");\n    return {}\n  }\n}\n\nvar computedWatcherOptions = { lazy: true };\n\nfunction initComputed (vm, computed) {\n  var watchers = vm._computedWatchers = Object.create(null);\n  // computed properties are just getters during SSR\n  var isSSR = isServerRendering();\n\n  for (var key in computed) {\n    var userDef = computed[key];\n    var getter = typeof userDef === 'function' ? userDef : userDef.get;\n    if (\"development\" !== 'production' && getter == null) {\n      warn(\n        (\"Getter is missing for computed property \\\"\" + key + \"\\\".\"),\n        vm\n      );\n    }\n\n    if (!isSSR) {\n      // create internal watcher for the computed property.\n      watchers[key] = new Watcher(\n        vm,\n        getter || noop,\n        noop,\n        computedWatcherOptions\n      );\n    }\n\n    // component-defined computed properties are already defined on the\n    // component prototype. We only need to define computed properties defined\n    // at instantiation here.\n    if (!(key in vm)) {\n      defineComputed(vm, key, userDef);\n    } else {\n      if (key in vm.$data) {\n        warn((\"The computed property \\\"\" + key + \"\\\" is already defined in data.\"), vm);\n      } else if (vm.$options.props && key in vm.$options.props) {\n        warn((\"The computed property \\\"\" + key + \"\\\" is already defined as a prop.\"), vm);\n      }\n    }\n  }\n}\n\nfunction defineComputed (\n  target,\n  key,\n  userDef\n) {\n  var shouldCache = !isServerRendering();\n  if (typeof userDef === 'function') {\n    sharedPropertyDefinition.get = shouldCache\n      ? createComputedGetter(key)\n      : userDef;\n    sharedPropertyDefinition.set = noop;\n  } else {\n    sharedPropertyDefinition.get = userDef.get\n      ? shouldCache && userDef.cache !== false\n        ? createComputedGetter(key)\n        : userDef.get\n      : noop;\n    sharedPropertyDefinition.set = userDef.set\n      ? userDef.set\n      : noop;\n  }\n  if (\"development\" !== 'production' &&\n      sharedPropertyDefinition.set === noop) {\n    sharedPropertyDefinition.set = function () {\n      warn(\n        (\"Computed property \\\"\" + key + \"\\\" was assigned to but it has no setter.\"),\n        this\n      );\n    };\n  }\n  Object.defineProperty(target, key, sharedPropertyDefinition);\n}\n\nfunction createComputedGetter (key) {\n  return function computedGetter () {\n    var watcher = this._computedWatchers && this._computedWatchers[key];\n    if (watcher) {\n      if (watcher.dirty) {\n        watcher.evaluate();\n      }\n      if (Dep.target) {\n        watcher.depend();\n      }\n      return watcher.value\n    }\n  }\n}\n\nfunction initMethods (vm, methods) {\n  var props = vm.$options.props;\n  for (var key in methods) {\n    {\n      if (methods[key] == null) {\n        warn(\n          \"Method \\\"\" + key + \"\\\" has an undefined value in the component definition. \" +\n          \"Did you reference the function correctly?\",\n          vm\n        );\n      }\n      if (props && hasOwn(props, key)) {\n        warn(\n          (\"Method \\\"\" + key + \"\\\" has already been defined as a prop.\"),\n          vm\n        );\n      }\n      if ((key in vm) && isReserved(key)) {\n        warn(\n          \"Method \\\"\" + key + \"\\\" conflicts with an existing Vue instance method. \" +\n          \"Avoid defining component methods that start with _ or $.\"\n        );\n      }\n    }\n    vm[key] = methods[key] == null ? noop : bind(methods[key], vm);\n  }\n}\n\nfunction initWatch (vm, watch) {\n  for (var key in watch) {\n    var handler = watch[key];\n    if (Array.isArray(handler)) {\n      for (var i = 0; i < handler.length; i++) {\n        createWatcher(vm, key, handler[i]);\n      }\n    } else {\n      createWatcher(vm, key, handler);\n    }\n  }\n}\n\nfunction createWatcher (\n  vm,\n  keyOrFn,\n  handler,\n  options\n) {\n  if (isPlainObject(handler)) {\n    options = handler;\n    handler = handler.handler;\n  }\n  if (typeof handler === 'string') {\n    handler = vm[handler];\n  }\n  return vm.$watch(keyOrFn, handler, options)\n}\n\nfunction stateMixin (Vue) {\n  // flow somehow has problems with directly declared definition object\n  // when using Object.defineProperty, so we have to procedurally build up\n  // the object here.\n  var dataDef = {};\n  dataDef.get = function () { return this._data };\n  var propsDef = {};\n  propsDef.get = function () { return this._props };\n  {\n    dataDef.set = function (newData) {\n      warn(\n        'Avoid replacing instance root $data. ' +\n        'Use nested data properties instead.',\n        this\n      );\n    };\n    propsDef.set = function () {\n      warn(\"$props is readonly.\", this);\n    };\n  }\n  Object.defineProperty(Vue.prototype, '$data', dataDef);\n  Object.defineProperty(Vue.prototype, '$props', propsDef);\n\n  Vue.prototype.$set = set;\n  Vue.prototype.$delete = del;\n\n  Vue.prototype.$watch = function (\n    expOrFn,\n    cb,\n    options\n  ) {\n    var vm = this;\n    if (isPlainObject(cb)) {\n      return createWatcher(vm, expOrFn, cb, options)\n    }\n    options = options || {};\n    options.user = true;\n    var watcher = new Watcher(vm, expOrFn, cb, options);\n    if (options.immediate) {\n      cb.call(vm, watcher.value);\n    }\n    return function unwatchFn () {\n      watcher.teardown();\n    }\n  };\n}\n\n/*  */\n\nfunction initProvide (vm) {\n  var provide = vm.$options.provide;\n  if (provide) {\n    vm._provided = typeof provide === 'function'\n      ? provide.call(vm)\n      : provide;\n  }\n}\n\nfunction initInjections (vm) {\n  var result = resolveInject(vm.$options.inject, vm);\n  if (result) {\n    observerState.shouldConvert = false;\n    Object.keys(result).forEach(function (key) {\n      /* istanbul ignore else */\n      {\n        defineReactive(vm, key, result[key], function () {\n          warn(\n            \"Avoid mutating an injected value directly since the changes will be \" +\n            \"overwritten whenever the provided component re-renders. \" +\n            \"injection being mutated: \\\"\" + key + \"\\\"\",\n            vm\n          );\n        });\n      }\n    });\n    observerState.shouldConvert = true;\n  }\n}\n\nfunction resolveInject (inject, vm) {\n  if (inject) {\n    // inject is :any because flow is not smart enough to figure out cached\n    var result = Object.create(null);\n    var keys = hasSymbol\n        ? Reflect.ownKeys(inject).filter(function (key) {\n          /* istanbul ignore next */\n          return Object.getOwnPropertyDescriptor(inject, key).enumerable\n        })\n        : Object.keys(inject);\n\n    for (var i = 0; i < keys.length; i++) {\n      var key = keys[i];\n      var provideKey = inject[key].from;\n      var source = vm;\n      while (source) {\n        if (source._provided && provideKey in source._provided) {\n          result[key] = source._provided[provideKey];\n          break\n        }\n        source = source.$parent;\n      }\n      if (!source) {\n        if ('default' in inject[key]) {\n          var provideDefault = inject[key].default;\n          result[key] = typeof provideDefault === 'function'\n            ? provideDefault.call(vm)\n            : provideDefault;\n        } else {\n          warn((\"Injection \\\"\" + key + \"\\\" not found\"), vm);\n        }\n      }\n    }\n    return result\n  }\n}\n\n/*  */\n\n/**\n * Runtime helper for rendering v-for lists.\n */\nfunction renderList (\n  val,\n  render\n) {\n  var ret, i, l, keys, key;\n  if (Array.isArray(val) || typeof val === 'string') {\n    ret = new Array(val.length);\n    for (i = 0, l = val.length; i < l; i++) {\n      ret[i] = render(val[i], i);\n    }\n  } else if (typeof val === 'number') {\n    ret = new Array(val);\n    for (i = 0; i < val; i++) {\n      ret[i] = render(i + 1, i);\n    }\n  } else if (isObject(val)) {\n    keys = Object.keys(val);\n    ret = new Array(keys.length);\n    for (i = 0, l = keys.length; i < l; i++) {\n      key = keys[i];\n      ret[i] = render(val[key], key, i);\n    }\n  }\n  if (isDef(ret)) {\n    (ret)._isVList = true;\n  }\n  return ret\n}\n\n/*  */\n\n/**\n * Runtime helper for rendering <slot>\n */\nfunction renderSlot (\n  name,\n  fallback,\n  props,\n  bindObject\n) {\n  var scopedSlotFn = this.$scopedSlots[name];\n  var nodes;\n  if (scopedSlotFn) { // scoped slot\n    props = props || {};\n    if (bindObject) {\n      if (\"development\" !== 'production' && !isObject(bindObject)) {\n        warn(\n          'slot v-bind without argument expects an Object',\n          this\n        );\n      }\n      props = extend(extend({}, bindObject), props);\n    }\n    nodes = scopedSlotFn(props) || fallback;\n  } else {\n    var slotNodes = this.$slots[name];\n    // warn duplicate slot usage\n    if (slotNodes) {\n      if (\"development\" !== 'production' && slotNodes._rendered) {\n        warn(\n          \"Duplicate presence of slot \\\"\" + name + \"\\\" found in the same render tree \" +\n          \"- this will likely cause render errors.\",\n          this\n        );\n      }\n      slotNodes._rendered = true;\n    }\n    nodes = slotNodes || fallback;\n  }\n\n  var target = props && props.slot;\n  if (target) {\n    return this.$createElement('template', { slot: target }, nodes)\n  } else {\n    return nodes\n  }\n}\n\n/*  */\n\n/**\n * Runtime helper for resolving filters\n */\nfunction resolveFilter (id) {\n  return resolveAsset(this.$options, 'filters', id, true) || identity\n}\n\n/*  */\n\n/**\n * Runtime helper for checking keyCodes from config.\n * exposed as Vue.prototype._k\n * passing in eventKeyName as last argument separately for backwards compat\n */\nfunction checkKeyCodes (\n  eventKeyCode,\n  key,\n  builtInAlias,\n  eventKeyName\n) {\n  var keyCodes = config.keyCodes[key] || builtInAlias;\n  if (keyCodes) {\n    if (Array.isArray(keyCodes)) {\n      return keyCodes.indexOf(eventKeyCode) === -1\n    } else {\n      return keyCodes !== eventKeyCode\n    }\n  } else if (eventKeyName) {\n    return hyphenate(eventKeyName) !== key\n  }\n}\n\n/*  */\n\n/**\n * Runtime helper for merging v-bind=\"object\" into a VNode's data.\n */\nfunction bindObjectProps (\n  data,\n  tag,\n  value,\n  asProp,\n  isSync\n) {\n  if (value) {\n    if (!isObject(value)) {\n      \"development\" !== 'production' && warn(\n        'v-bind without argument expects an Object or Array value',\n        this\n      );\n    } else {\n      if (Array.isArray(value)) {\n        value = toObject(value);\n      }\n      var hash;\n      var loop = function ( key ) {\n        if (\n          key === 'class' ||\n          key === 'style' ||\n          isReservedAttribute(key)\n        ) {\n          hash = data;\n        } else {\n          var type = data.attrs && data.attrs.type;\n          hash = asProp || config.mustUseProp(tag, type, key)\n            ? data.domProps || (data.domProps = {})\n            : data.attrs || (data.attrs = {});\n        }\n        if (!(key in hash)) {\n          hash[key] = value[key];\n\n          if (isSync) {\n            var on = data.on || (data.on = {});\n            on[(\"update:\" + key)] = function ($event) {\n              value[key] = $event;\n            };\n          }\n        }\n      };\n\n      for (var key in value) loop( key );\n    }\n  }\n  return data\n}\n\n/*  */\n\n/**\n * Runtime helper for rendering static trees.\n */\nfunction renderStatic (\n  index,\n  isInFor\n) {\n  var cached = this._staticTrees || (this._staticTrees = []);\n  var tree = cached[index];\n  // if has already-rendered static tree and not inside v-for,\n  // we can reuse the same tree by doing a shallow clone.\n  if (tree && !isInFor) {\n    return Array.isArray(tree)\n      ? cloneVNodes(tree)\n      : cloneVNode(tree)\n  }\n  // otherwise, render a fresh tree.\n  tree = cached[index] = this.$options.staticRenderFns[index].call(\n    this._renderProxy,\n    null,\n    this // for render fns generated for functional component templates\n  );\n  markStatic(tree, (\"__static__\" + index), false);\n  return tree\n}\n\n/**\n * Runtime helper for v-once.\n * Effectively it means marking the node as static with a unique key.\n */\nfunction markOnce (\n  tree,\n  index,\n  key\n) {\n  markStatic(tree, (\"__once__\" + index + (key ? (\"_\" + key) : \"\")), true);\n  return tree\n}\n\nfunction markStatic (\n  tree,\n  key,\n  isOnce\n) {\n  if (Array.isArray(tree)) {\n    for (var i = 0; i < tree.length; i++) {\n      if (tree[i] && typeof tree[i] !== 'string') {\n        markStaticNode(tree[i], (key + \"_\" + i), isOnce);\n      }\n    }\n  } else {\n    markStaticNode(tree, key, isOnce);\n  }\n}\n\nfunction markStaticNode (node, key, isOnce) {\n  node.isStatic = true;\n  node.key = key;\n  node.isOnce = isOnce;\n}\n\n/*  */\n\nfunction bindObjectListeners (data, value) {\n  if (value) {\n    if (!isPlainObject(value)) {\n      \"development\" !== 'production' && warn(\n        'v-on without argument expects an Object value',\n        this\n      );\n    } else {\n      var on = data.on = data.on ? extend({}, data.on) : {};\n      for (var key in value) {\n        var existing = on[key];\n        var ours = value[key];\n        on[key] = existing ? [].concat(existing, ours) : ours;\n      }\n    }\n  }\n  return data\n}\n\n/*  */\n\nfunction installRenderHelpers (target) {\n  target._o = markOnce;\n  target._n = toNumber;\n  target._s = toString;\n  target._l = renderList;\n  target._t = renderSlot;\n  target._q = looseEqual;\n  target._i = looseIndexOf;\n  target._m = renderStatic;\n  target._f = resolveFilter;\n  target._k = checkKeyCodes;\n  target._b = bindObjectProps;\n  target._v = createTextVNode;\n  target._e = createEmptyVNode;\n  target._u = resolveScopedSlots;\n  target._g = bindObjectListeners;\n}\n\n/*  */\n\nfunction FunctionalRenderContext (\n  data,\n  props,\n  children,\n  parent,\n  Ctor\n) {\n  var options = Ctor.options;\n  this.data = data;\n  this.props = props;\n  this.children = children;\n  this.parent = parent;\n  this.listeners = data.on || emptyObject;\n  this.injections = resolveInject(options.inject, parent);\n  this.slots = function () { return resolveSlots(children, parent); };\n\n  // ensure the createElement function in functional components\n  // gets a unique context - this is necessary for correct named slot check\n  var contextVm = Object.create(parent);\n  var isCompiled = isTrue(options._compiled);\n  var needNormalization = !isCompiled;\n\n  // support for compiled functional template\n  if (isCompiled) {\n    // exposing $options for renderStatic()\n    this.$options = options;\n    // pre-resolve slots for renderSlot()\n    this.$slots = this.slots();\n    this.$scopedSlots = data.scopedSlots || emptyObject;\n  }\n\n  if (options._scopeId) {\n    this._c = function (a, b, c, d) {\n      var vnode = createElement(contextVm, a, b, c, d, needNormalization);\n      if (vnode) {\n        vnode.fnScopeId = options._scopeId;\n        vnode.fnContext = parent;\n      }\n      return vnode\n    };\n  } else {\n    this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };\n  }\n}\n\ninstallRenderHelpers(FunctionalRenderContext.prototype);\n\nfunction createFunctionalComponent (\n  Ctor,\n  propsData,\n  data,\n  contextVm,\n  children\n) {\n  var options = Ctor.options;\n  var props = {};\n  var propOptions = options.props;\n  if (isDef(propOptions)) {\n    for (var key in propOptions) {\n      props[key] = validateProp(key, propOptions, propsData || emptyObject);\n    }\n  } else {\n    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }\n    if (isDef(data.props)) { mergeProps(props, data.props); }\n  }\n\n  var renderContext = new FunctionalRenderContext(\n    data,\n    props,\n    children,\n    contextVm,\n    Ctor\n  );\n\n  var vnode = options.render.call(null, renderContext._c, renderContext);\n\n  if (vnode instanceof VNode) {\n    vnode.fnContext = contextVm;\n    vnode.fnOptions = options;\n    if (data.slot) {\n      (vnode.data || (vnode.data = {})).slot = data.slot;\n    }\n  }\n\n  return vnode\n}\n\nfunction mergeProps (to, from) {\n  for (var key in from) {\n    to[camelize(key)] = from[key];\n  }\n}\n\n/*  */\n\n// hooks to be invoked on component VNodes during patch\nvar componentVNodeHooks = {\n  init: function init (\n    vnode,\n    hydrating,\n    parentElm,\n    refElm\n  ) {\n    if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {\n      var child = vnode.componentInstance = createComponentInstanceForVnode(\n        vnode,\n        activeInstance,\n        parentElm,\n        refElm\n      );\n      child.$mount(hydrating ? vnode.elm : undefined, hydrating);\n    } else if (vnode.data.keepAlive) {\n      // kept-alive components, treat as a patch\n      var mountedNode = vnode; // work around flow\n      componentVNodeHooks.prepatch(mountedNode, mountedNode);\n    }\n  },\n\n  prepatch: function prepatch (oldVnode, vnode) {\n    var options = vnode.componentOptions;\n    var child = vnode.componentInstance = oldVnode.componentInstance;\n    updateChildComponent(\n      child,\n      options.propsData, // updated props\n      options.listeners, // updated listeners\n      vnode, // new parent vnode\n      options.children // new children\n    );\n  },\n\n  insert: function insert (vnode) {\n    var context = vnode.context;\n    var componentInstance = vnode.componentInstance;\n    if (!componentInstance._isMounted) {\n      componentInstance._isMounted = true;\n      callHook(componentInstance, 'mounted');\n    }\n    if (vnode.data.keepAlive) {\n      if (context._isMounted) {\n        // vue-router#1212\n        // During updates, a kept-alive component's child components may\n        // change, so directly walking the tree here may call activated hooks\n        // on incorrect children. Instead we push them into a queue which will\n        // be processed after the whole patch process ended.\n        queueActivatedComponent(componentInstance);\n      } else {\n        activateChildComponent(componentInstance, true /* direct */);\n      }\n    }\n  },\n\n  destroy: function destroy (vnode) {\n    var componentInstance = vnode.componentInstance;\n    if (!componentInstance._isDestroyed) {\n      if (!vnode.data.keepAlive) {\n        componentInstance.$destroy();\n      } else {\n        deactivateChildComponent(componentInstance, true /* direct */);\n      }\n    }\n  }\n};\n\nvar hooksToMerge = Object.keys(componentVNodeHooks);\n\nfunction createComponent (\n  Ctor,\n  data,\n  context,\n  children,\n  tag\n) {\n  if (isUndef(Ctor)) {\n    return\n  }\n\n  var baseCtor = context.$options._base;\n\n  // plain options object: turn it into a constructor\n  if (isObject(Ctor)) {\n    Ctor = baseCtor.extend(Ctor);\n  }\n\n  // if at this stage it's not a constructor or an async component factory,\n  // reject.\n  if (typeof Ctor !== 'function') {\n    {\n      warn((\"Invalid Component definition: \" + (String(Ctor))), context);\n    }\n    return\n  }\n\n  // async component\n  var asyncFactory;\n  if (isUndef(Ctor.cid)) {\n    asyncFactory = Ctor;\n    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);\n    if (Ctor === undefined) {\n      // return a placeholder node for async component, which is rendered\n      // as a comment node but preserves all the raw information for the node.\n      // the information will be used for async server-rendering and hydration.\n      return createAsyncPlaceholder(\n        asyncFactory,\n        data,\n        context,\n        children,\n        tag\n      )\n    }\n  }\n\n  data = data || {};\n\n  // resolve constructor options in case global mixins are applied after\n  // component constructor creation\n  resolveConstructorOptions(Ctor);\n\n  // transform component v-model data into props & events\n  if (isDef(data.model)) {\n    transformModel(Ctor.options, data);\n  }\n\n  // extract props\n  var propsData = extractPropsFromVNodeData(data, Ctor, tag);\n\n  // functional component\n  if (isTrue(Ctor.options.functional)) {\n    return createFunctionalComponent(Ctor, propsData, data, context, children)\n  }\n\n  // extract listeners, since these needs to be treated as\n  // child component listeners instead of DOM listeners\n  var listeners = data.on;\n  // replace with listeners with .native modifier\n  // so it gets processed during parent component patch.\n  data.on = data.nativeOn;\n\n  if (isTrue(Ctor.options.abstract)) {\n    // abstract components do not keep anything\n    // other than props & listeners & slot\n\n    // work around flow\n    var slot = data.slot;\n    data = {};\n    if (slot) {\n      data.slot = slot;\n    }\n  }\n\n  // merge component management hooks onto the placeholder node\n  mergeHooks(data);\n\n  // return a placeholder vnode\n  var name = Ctor.options.name || tag;\n  var vnode = new VNode(\n    (\"vue-component-\" + (Ctor.cid) + (name ? (\"-\" + name) : '')),\n    data, undefined, undefined, undefined, context,\n    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },\n    asyncFactory\n  );\n  return vnode\n}\n\nfunction createComponentInstanceForVnode (\n  vnode, // we know it's MountedComponentVNode but flow doesn't\n  parent, // activeInstance in lifecycle state\n  parentElm,\n  refElm\n) {\n  var options = {\n    _isComponent: true,\n    parent: parent,\n    _parentVnode: vnode,\n    _parentElm: parentElm || null,\n    _refElm: refElm || null\n  };\n  // check inline-template render functions\n  var inlineTemplate = vnode.data.inlineTemplate;\n  if (isDef(inlineTemplate)) {\n    options.render = inlineTemplate.render;\n    options.staticRenderFns = inlineTemplate.staticRenderFns;\n  }\n  return new vnode.componentOptions.Ctor(options)\n}\n\nfunction mergeHooks (data) {\n  if (!data.hook) {\n    data.hook = {};\n  }\n  for (var i = 0; i < hooksToMerge.length; i++) {\n    var key = hooksToMerge[i];\n    var fromParent = data.hook[key];\n    var ours = componentVNodeHooks[key];\n    data.hook[key] = fromParent ? mergeHook$1(ours, fromParent) : ours;\n  }\n}\n\nfunction mergeHook$1 (one, two) {\n  return function (a, b, c, d) {\n    one(a, b, c, d);\n    two(a, b, c, d);\n  }\n}\n\n// transform component v-model info (value and callback) into\n// prop and event handler respectively.\nfunction transformModel (options, data) {\n  var prop = (options.model && options.model.prop) || 'value';\n  var event = (options.model && options.model.event) || 'input';(data.props || (data.props = {}))[prop] = data.model.value;\n  var on = data.on || (data.on = {});\n  if (isDef(on[event])) {\n    on[event] = [data.model.callback].concat(on[event]);\n  } else {\n    on[event] = data.model.callback;\n  }\n}\n\n/*  */\n\nvar SIMPLE_NORMALIZE = 1;\nvar ALWAYS_NORMALIZE = 2;\n\n// wrapper function for providing a more flexible interface\n// without getting yelled at by flow\nfunction createElement (\n  context,\n  tag,\n  data,\n  children,\n  normalizationType,\n  alwaysNormalize\n) {\n  if (Array.isArray(data) || isPrimitive(data)) {\n    normalizationType = children;\n    children = data;\n    data = undefined;\n  }\n  if (isTrue(alwaysNormalize)) {\n    normalizationType = ALWAYS_NORMALIZE;\n  }\n  return _createElement(context, tag, data, children, normalizationType)\n}\n\nfunction _createElement (\n  context,\n  tag,\n  data,\n  children,\n  normalizationType\n) {\n  if (isDef(data) && isDef((data).__ob__)) {\n    \"development\" !== 'production' && warn(\n      \"Avoid using observed data object as vnode data: \" + (JSON.stringify(data)) + \"\\n\" +\n      'Always create fresh vnode data objects in each render!',\n      context\n    );\n    return createEmptyVNode()\n  }\n  // object syntax in v-bind\n  if (isDef(data) && isDef(data.is)) {\n    tag = data.is;\n  }\n  if (!tag) {\n    // in case of component :is set to falsy value\n    return createEmptyVNode()\n  }\n  // warn against non-primitive key\n  if (\"development\" !== 'production' &&\n    isDef(data) && isDef(data.key) && !isPrimitive(data.key)\n  ) {\n    warn(\n      'Avoid using non-primitive value as key, ' +\n      'use string/number value instead.',\n      context\n    );\n  }\n  // support single function children as default scoped slot\n  if (Array.isArray(children) &&\n    typeof children[0] === 'function'\n  ) {\n    data = data || {};\n    data.scopedSlots = { default: children[0] };\n    children.length = 0;\n  }\n  if (normalizationType === ALWAYS_NORMALIZE) {\n    children = normalizeChildren(children);\n  } else if (normalizationType === SIMPLE_NORMALIZE) {\n    children = simpleNormalizeChildren(children);\n  }\n  var vnode, ns;\n  if (typeof tag === 'string') {\n    var Ctor;\n    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);\n    if (config.isReservedTag(tag)) {\n      // platform built-in elements\n      vnode = new VNode(\n        config.parsePlatformTagName(tag), data, children,\n        undefined, undefined, context\n      );\n    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {\n      // component\n      vnode = createComponent(Ctor, data, context, children, tag);\n    } else {\n      // unknown or unlisted namespaced elements\n      // check at runtime because it may get assigned a namespace when its\n      // parent normalizes children\n      vnode = new VNode(\n        tag, data, children,\n        undefined, undefined, context\n      );\n    }\n  } else {\n    // direct component options / constructor\n    vnode = createComponent(tag, data, context, children);\n  }\n  if (isDef(vnode)) {\n    if (ns) { applyNS(vnode, ns); }\n    return vnode\n  } else {\n    return createEmptyVNode()\n  }\n}\n\nfunction applyNS (vnode, ns, force) {\n  vnode.ns = ns;\n  if (vnode.tag === 'foreignObject') {\n    // use default namespace inside foreignObject\n    ns = undefined;\n    force = true;\n  }\n  if (isDef(vnode.children)) {\n    for (var i = 0, l = vnode.children.length; i < l; i++) {\n      var child = vnode.children[i];\n      if (isDef(child.tag) && (isUndef(child.ns) || isTrue(force))) {\n        applyNS(child, ns, force);\n      }\n    }\n  }\n}\n\n/*  */\n\nfunction initRender (vm) {\n  vm._vnode = null; // the root of the child tree\n  vm._staticTrees = null; // v-once cached trees\n  var options = vm.$options;\n  var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree\n  var renderContext = parentVnode && parentVnode.context;\n  vm.$slots = resolveSlots(options._renderChildren, renderContext);\n  vm.$scopedSlots = emptyObject;\n  // bind the createElement fn to this instance\n  // so that we get proper render context inside it.\n  // args order: tag, data, children, normalizationType, alwaysNormalize\n  // internal version is used by render functions compiled from templates\n  vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };\n  // normalization is always applied for the public version, used in\n  // user-written render functions.\n  vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };\n\n  // $attrs & $listeners are exposed for easier HOC creation.\n  // they need to be reactive so that HOCs using them are always updated\n  var parentData = parentVnode && parentVnode.data;\n\n  /* istanbul ignore else */\n  {\n    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {\n      !isUpdatingChildComponent && warn(\"$attrs is readonly.\", vm);\n    }, true);\n    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, function () {\n      !isUpdatingChildComponent && warn(\"$listeners is readonly.\", vm);\n    }, true);\n  }\n}\n\nfunction renderMixin (Vue) {\n  // install runtime convenience helpers\n  installRenderHelpers(Vue.prototype);\n\n  Vue.prototype.$nextTick = function (fn) {\n    return nextTick(fn, this)\n  };\n\n  Vue.prototype._render = function () {\n    var vm = this;\n    var ref = vm.$options;\n    var render = ref.render;\n    var _parentVnode = ref._parentVnode;\n\n    if (vm._isMounted) {\n      // if the parent didn't update, the slot nodes will be the ones from\n      // last render. They need to be cloned to ensure \"freshness\" for this render.\n      for (var key in vm.$slots) {\n        var slot = vm.$slots[key];\n        // _rendered is a flag added by renderSlot, but may not be present\n        // if the slot is passed from manually written render functions\n        if (slot._rendered || (slot[0] && slot[0].elm)) {\n          vm.$slots[key] = cloneVNodes(slot, true /* deep */);\n        }\n      }\n    }\n\n    vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject;\n\n    // set parent vnode. this allows render functions to have access\n    // to the data on the placeholder node.\n    vm.$vnode = _parentVnode;\n    // render self\n    var vnode;\n    try {\n      vnode = render.call(vm._renderProxy, vm.$createElement);\n    } catch (e) {\n      handleError(e, vm, \"render\");\n      // return error render result,\n      // or previous vnode to prevent render error causing blank component\n      /* istanbul ignore else */\n      {\n        if (vm.$options.renderError) {\n          try {\n            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);\n          } catch (e) {\n            handleError(e, vm, \"renderError\");\n            vnode = vm._vnode;\n          }\n        } else {\n          vnode = vm._vnode;\n        }\n      }\n    }\n    // return empty vnode in case the render function errored out\n    if (!(vnode instanceof VNode)) {\n      if (\"development\" !== 'production' && Array.isArray(vnode)) {\n        warn(\n          'Multiple root nodes returned from render function. Render function ' +\n          'should return a single root node.',\n          vm\n        );\n      }\n      vnode = createEmptyVNode();\n    }\n    // set parent\n    vnode.parent = _parentVnode;\n    return vnode\n  };\n}\n\n/*  */\n\nvar uid$1 = 0;\n\nfunction initMixin (Vue) {\n  Vue.prototype._init = function (options) {\n    var vm = this;\n    // a uid\n    vm._uid = uid$1++;\n\n    var startTag, endTag;\n    /* istanbul ignore if */\n    if (\"development\" !== 'production' && config.performance && mark) {\n      startTag = \"vue-perf-start:\" + (vm._uid);\n      endTag = \"vue-perf-end:\" + (vm._uid);\n      mark(startTag);\n    }\n\n    // a flag to avoid this being observed\n    vm._isVue = true;\n    // merge options\n    if (options && options._isComponent) {\n      // optimize internal component instantiation\n      // since dynamic options merging is pretty slow, and none of the\n      // internal component options needs special treatment.\n      initInternalComponent(vm, options);\n    } else {\n      vm.$options = mergeOptions(\n        resolveConstructorOptions(vm.constructor),\n        options || {},\n        vm\n      );\n    }\n    /* istanbul ignore else */\n    {\n      initProxy(vm);\n    }\n    // expose real self\n    vm._self = vm;\n    initLifecycle(vm);\n    initEvents(vm);\n    initRender(vm);\n    callHook(vm, 'beforeCreate');\n    initInjections(vm); // resolve injections before data/props\n    initState(vm);\n    initProvide(vm); // resolve provide after data/props\n    callHook(vm, 'created');\n\n    /* istanbul ignore if */\n    if (\"development\" !== 'production' && config.performance && mark) {\n      vm._name = formatComponentName(vm, false);\n      mark(endTag);\n      measure((\"vue \" + (vm._name) + \" init\"), startTag, endTag);\n    }\n\n    if (vm.$options.el) {\n      vm.$mount(vm.$options.el);\n    }\n  };\n}\n\nfunction initInternalComponent (vm, options) {\n  var opts = vm.$options = Object.create(vm.constructor.options);\n  // doing this because it's faster than dynamic enumeration.\n  var parentVnode = options._parentVnode;\n  opts.parent = options.parent;\n  opts._parentVnode = parentVnode;\n  opts._parentElm = options._parentElm;\n  opts._refElm = options._refElm;\n\n  var vnodeComponentOptions = parentVnode.componentOptions;\n  opts.propsData = vnodeComponentOptions.propsData;\n  opts._parentListeners = vnodeComponentOptions.listeners;\n  opts._renderChildren = vnodeComponentOptions.children;\n  opts._componentTag = vnodeComponentOptions.tag;\n\n  if (options.render) {\n    opts.render = options.render;\n    opts.staticRenderFns = options.staticRenderFns;\n  }\n}\n\nfunction resolveConstructorOptions (Ctor) {\n  var options = Ctor.options;\n  if (Ctor.super) {\n    var superOptions = resolveConstructorOptions(Ctor.super);\n    var cachedSuperOptions = Ctor.superOptions;\n    if (superOptions !== cachedSuperOptions) {\n      // super option changed,\n      // need to resolve new options.\n      Ctor.superOptions = superOptions;\n      // check if there are any late-modified/attached options (#4976)\n      var modifiedOptions = resolveModifiedOptions(Ctor);\n      // update base extend options\n      if (modifiedOptions) {\n        extend(Ctor.extendOptions, modifiedOptions);\n      }\n      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);\n      if (options.name) {\n        options.components[options.name] = Ctor;\n      }\n    }\n  }\n  return options\n}\n\nfunction resolveModifiedOptions (Ctor) {\n  var modified;\n  var latest = Ctor.options;\n  var extended = Ctor.extendOptions;\n  var sealed = Ctor.sealedOptions;\n  for (var key in latest) {\n    if (latest[key] !== sealed[key]) {\n      if (!modified) { modified = {}; }\n      modified[key] = dedupe(latest[key], extended[key], sealed[key]);\n    }\n  }\n  return modified\n}\n\nfunction dedupe (latest, extended, sealed) {\n  // compare latest and sealed to ensure lifecycle hooks won't be duplicated\n  // between merges\n  if (Array.isArray(latest)) {\n    var res = [];\n    sealed = Array.isArray(sealed) ? sealed : [sealed];\n    extended = Array.isArray(extended) ? extended : [extended];\n    for (var i = 0; i < latest.length; i++) {\n      // push original options and not sealed options to exclude duplicated options\n      if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {\n        res.push(latest[i]);\n      }\n    }\n    return res\n  } else {\n    return latest\n  }\n}\n\nfunction Vue$3 (options) {\n  if (\"development\" !== 'production' &&\n    !(this instanceof Vue$3)\n  ) {\n    warn('Vue is a constructor and should be called with the `new` keyword');\n  }\n  this._init(options);\n}\n\ninitMixin(Vue$3);\nstateMixin(Vue$3);\neventsMixin(Vue$3);\nlifecycleMixin(Vue$3);\nrenderMixin(Vue$3);\n\n/*  */\n\nfunction initUse (Vue) {\n  Vue.use = function (plugin) {\n    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));\n    if (installedPlugins.indexOf(plugin) > -1) {\n      return this\n    }\n\n    // additional parameters\n    var args = toArray(arguments, 1);\n    args.unshift(this);\n    if (typeof plugin.install === 'function') {\n      plugin.install.apply(plugin, args);\n    } else if (typeof plugin === 'function') {\n      plugin.apply(null, args);\n    }\n    installedPlugins.push(plugin);\n    return this\n  };\n}\n\n/*  */\n\nfunction initMixin$1 (Vue) {\n  Vue.mixin = function (mixin) {\n    this.options = mergeOptions(this.options, mixin);\n    return this\n  };\n}\n\n/*  */\n\nfunction initExtend (Vue) {\n  /**\n   * Each instance constructor, including Vue, has a unique\n   * cid. This enables us to create wrapped \"child\n   * constructors\" for prototypal inheritance and cache them.\n   */\n  Vue.cid = 0;\n  var cid = 1;\n\n  /**\n   * Class inheritance\n   */\n  Vue.extend = function (extendOptions) {\n    extendOptions = extendOptions || {};\n    var Super = this;\n    var SuperId = Super.cid;\n    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});\n    if (cachedCtors[SuperId]) {\n      return cachedCtors[SuperId]\n    }\n\n    var name = extendOptions.name || Super.options.name;\n    if (\"development\" !== 'production' && name) {\n      validateComponentName(name);\n    }\n\n    var Sub = function VueComponent (options) {\n      this._init(options);\n    };\n    Sub.prototype = Object.create(Super.prototype);\n    Sub.prototype.constructor = Sub;\n    Sub.cid = cid++;\n    Sub.options = mergeOptions(\n      Super.options,\n      extendOptions\n    );\n    Sub['super'] = Super;\n\n    // For props and computed properties, we define the proxy getters on\n    // the Vue instances at extension time, on the extended prototype. This\n    // avoids Object.defineProperty calls for each instance created.\n    if (Sub.options.props) {\n      initProps$1(Sub);\n    }\n    if (Sub.options.computed) {\n      initComputed$1(Sub);\n    }\n\n    // allow further extension/mixin/plugin usage\n    Sub.extend = Super.extend;\n    Sub.mixin = Super.mixin;\n    Sub.use = Super.use;\n\n    // create asset registers, so extended classes\n    // can have their private assets too.\n    ASSET_TYPES.forEach(function (type) {\n      Sub[type] = Super[type];\n    });\n    // enable recursive self-lookup\n    if (name) {\n      Sub.options.components[name] = Sub;\n    }\n\n    // keep a reference to the super options at extension time.\n    // later at instantiation we can check if Super's options have\n    // been updated.\n    Sub.superOptions = Super.options;\n    Sub.extendOptions = extendOptions;\n    Sub.sealedOptions = extend({}, Sub.options);\n\n    // cache constructor\n    cachedCtors[SuperId] = Sub;\n    return Sub\n  };\n}\n\nfunction initProps$1 (Comp) {\n  var props = Comp.options.props;\n  for (var key in props) {\n    proxy(Comp.prototype, \"_props\", key);\n  }\n}\n\nfunction initComputed$1 (Comp) {\n  var computed = Comp.options.computed;\n  for (var key in computed) {\n    defineComputed(Comp.prototype, key, computed[key]);\n  }\n}\n\n/*  */\n\nfunction initAssetRegisters (Vue) {\n  /**\n   * Create asset registration methods.\n   */\n  ASSET_TYPES.forEach(function (type) {\n    Vue[type] = function (\n      id,\n      definition\n    ) {\n      if (!definition) {\n        return this.options[type + 's'][id]\n      } else {\n        /* istanbul ignore if */\n        if (\"development\" !== 'production' && type === 'component') {\n          validateComponentName(id);\n        }\n        if (type === 'component' && isPlainObject(definition)) {\n          definition.name = definition.name || id;\n          definition = this.options._base.extend(definition);\n        }\n        if (type === 'directive' && typeof definition === 'function') {\n          definition = { bind: definition, update: definition };\n        }\n        this.options[type + 's'][id] = definition;\n        return definition\n      }\n    };\n  });\n}\n\n/*  */\n\nfunction getComponentName (opts) {\n  return opts && (opts.Ctor.options.name || opts.tag)\n}\n\nfunction matches (pattern, name) {\n  if (Array.isArray(pattern)) {\n    return pattern.indexOf(name) > -1\n  } else if (typeof pattern === 'string') {\n    return pattern.split(',').indexOf(name) > -1\n  } else if (isRegExp(pattern)) {\n    return pattern.test(name)\n  }\n  /* istanbul ignore next */\n  return false\n}\n\nfunction pruneCache (keepAliveInstance, filter) {\n  var cache = keepAliveInstance.cache;\n  var keys = keepAliveInstance.keys;\n  var _vnode = keepAliveInstance._vnode;\n  for (var key in cache) {\n    var cachedNode = cache[key];\n    if (cachedNode) {\n      var name = getComponentName(cachedNode.componentOptions);\n      if (name && !filter(name)) {\n        pruneCacheEntry(cache, key, keys, _vnode);\n      }\n    }\n  }\n}\n\nfunction pruneCacheEntry (\n  cache,\n  key,\n  keys,\n  current\n) {\n  var cached$$1 = cache[key];\n  if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {\n    cached$$1.componentInstance.$destroy();\n  }\n  cache[key] = null;\n  remove(keys, key);\n}\n\nvar patternTypes = [String, RegExp, Array];\n\nvar KeepAlive = {\n  name: 'keep-alive',\n  abstract: true,\n\n  props: {\n    include: patternTypes,\n    exclude: patternTypes,\n    max: [String, Number]\n  },\n\n  created: function created () {\n    this.cache = Object.create(null);\n    this.keys = [];\n  },\n\n  destroyed: function destroyed () {\n    var this$1 = this;\n\n    for (var key in this$1.cache) {\n      pruneCacheEntry(this$1.cache, key, this$1.keys);\n    }\n  },\n\n  watch: {\n    include: function include (val) {\n      pruneCache(this, function (name) { return matches(val, name); });\n    },\n    exclude: function exclude (val) {\n      pruneCache(this, function (name) { return !matches(val, name); });\n    }\n  },\n\n  render: function render () {\n    var slot = this.$slots.default;\n    var vnode = getFirstComponentChild(slot);\n    var componentOptions = vnode && vnode.componentOptions;\n    if (componentOptions) {\n      // check pattern\n      var name = getComponentName(componentOptions);\n      var ref = this;\n      var include = ref.include;\n      var exclude = ref.exclude;\n      if (\n        // not included\n        (include && (!name || !matches(include, name))) ||\n        // excluded\n        (exclude && name && matches(exclude, name))\n      ) {\n        return vnode\n      }\n\n      var ref$1 = this;\n      var cache = ref$1.cache;\n      var keys = ref$1.keys;\n      var key = vnode.key == null\n        // same constructor may get registered as different local components\n        // so cid alone is not enough (#3269)\n        ? componentOptions.Ctor.cid + (componentOptions.tag ? (\"::\" + (componentOptions.tag)) : '')\n        : vnode.key;\n      if (cache[key]) {\n        vnode.componentInstance = cache[key].componentInstance;\n        // make current key freshest\n        remove(keys, key);\n        keys.push(key);\n      } else {\n        cache[key] = vnode;\n        keys.push(key);\n        // prune oldest entry\n        if (this.max && keys.length > parseInt(this.max)) {\n          pruneCacheEntry(cache, keys[0], keys, this._vnode);\n        }\n      }\n\n      vnode.data.keepAlive = true;\n    }\n    return vnode || (slot && slot[0])\n  }\n};\n\nvar builtInComponents = {\n  KeepAlive: KeepAlive\n};\n\n/*  */\n\nfunction initGlobalAPI (Vue) {\n  // config\n  var configDef = {};\n  configDef.get = function () { return config; };\n  {\n    configDef.set = function () {\n      warn(\n        'Do not replace the Vue.config object, set individual fields instead.'\n      );\n    };\n  }\n  Object.defineProperty(Vue, 'config', configDef);\n\n  // exposed util methods.\n  // NOTE: these are not considered part of the public API - avoid relying on\n  // them unless you are aware of the risk.\n  Vue.util = {\n    warn: warn,\n    extend: extend,\n    mergeOptions: mergeOptions,\n    defineReactive: defineReactive\n  };\n\n  Vue.set = set;\n  Vue.delete = del;\n  Vue.nextTick = nextTick;\n\n  Vue.options = Object.create(null);\n  ASSET_TYPES.forEach(function (type) {\n    Vue.options[type + 's'] = Object.create(null);\n  });\n\n  // this is used to identify the \"base\" constructor to extend all plain-object\n  // components with in Weex's multi-instance scenarios.\n  Vue.options._base = Vue;\n\n  extend(Vue.options.components, builtInComponents);\n\n  initUse(Vue);\n  initMixin$1(Vue);\n  initExtend(Vue);\n  initAssetRegisters(Vue);\n}\n\ninitGlobalAPI(Vue$3);\n\nObject.defineProperty(Vue$3.prototype, '$isServer', {\n  get: isServerRendering\n});\n\nObject.defineProperty(Vue$3.prototype, '$ssrContext', {\n  get: function get () {\n    /* istanbul ignore next */\n    return this.$vnode && this.$vnode.ssrContext\n  }\n});\n\nVue$3.version = '2.5.10';\n\n/*  */\n\n// these are reserved for web because they are directly compiled away\n// during template compilation\nvar isReservedAttr = makeMap('style,class');\n\n// attributes that should be using props for binding\nvar acceptValue = makeMap('input,textarea,option,select,progress');\nvar mustUseProp = function (tag, type, attr) {\n  return (\n    (attr === 'value' && acceptValue(tag)) && type !== 'button' ||\n    (attr === 'selected' && tag === 'option') ||\n    (attr === 'checked' && tag === 'input') ||\n    (attr === 'muted' && tag === 'video')\n  )\n};\n\nvar isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck');\n\nvar isBooleanAttr = makeMap(\n  'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' +\n  'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' +\n  'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' +\n  'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' +\n  'required,reversed,scoped,seamless,selected,sortable,translate,' +\n  'truespeed,typemustmatch,visible'\n);\n\nvar xlinkNS = 'http://www.w3.org/1999/xlink';\n\nvar isXlink = function (name) {\n  return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink'\n};\n\nvar getXlinkProp = function (name) {\n  return isXlink(name) ? name.slice(6, name.length) : ''\n};\n\nvar isFalsyAttrValue = function (val) {\n  return val == null || val === false\n};\n\n/*  */\n\nfunction genClassForVnode (vnode) {\n  var data = vnode.data;\n  var parentNode = vnode;\n  var childNode = vnode;\n  while (isDef(childNode.componentInstance)) {\n    childNode = childNode.componentInstance._vnode;\n    if (childNode.data) {\n      data = mergeClassData(childNode.data, data);\n    }\n  }\n  while (isDef(parentNode = parentNode.parent)) {\n    if (parentNode.data) {\n      data = mergeClassData(data, parentNode.data);\n    }\n  }\n  return renderClass(data.staticClass, data.class)\n}\n\nfunction mergeClassData (child, parent) {\n  return {\n    staticClass: concat(child.staticClass, parent.staticClass),\n    class: isDef(child.class)\n      ? [child.class, parent.class]\n      : parent.class\n  }\n}\n\nfunction renderClass (\n  staticClass,\n  dynamicClass\n) {\n  if (isDef(staticClass) || isDef(dynamicClass)) {\n    return concat(staticClass, stringifyClass(dynamicClass))\n  }\n  /* istanbul ignore next */\n  return ''\n}\n\nfunction concat (a, b) {\n  return a ? b ? (a + ' ' + b) : a : (b || '')\n}\n\nfunction stringifyClass (value) {\n  if (Array.isArray(value)) {\n    return stringifyArray(value)\n  }\n  if (isObject(value)) {\n    return stringifyObject(value)\n  }\n  if (typeof value === 'string') {\n    return value\n  }\n  /* istanbul ignore next */\n  return ''\n}\n\nfunction stringifyArray (value) {\n  var res = '';\n  var stringified;\n  for (var i = 0, l = value.length; i < l; i++) {\n    if (isDef(stringified = stringifyClass(value[i])) && stringified !== '') {\n      if (res) { res += ' '; }\n      res += stringified;\n    }\n  }\n  return res\n}\n\nfunction stringifyObject (value) {\n  var res = '';\n  for (var key in value) {\n    if (value[key]) {\n      if (res) { res += ' '; }\n      res += key;\n    }\n  }\n  return res\n}\n\n/*  */\n\nvar namespaceMap = {\n  svg: 'http://www.w3.org/2000/svg',\n  math: 'http://www.w3.org/1998/Math/MathML'\n};\n\nvar isHTMLTag = makeMap(\n  'html,body,base,head,link,meta,style,title,' +\n  'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +\n  'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +\n  'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +\n  's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +\n  'embed,object,param,source,canvas,script,noscript,del,ins,' +\n  'caption,col,colgroup,table,thead,tbody,td,th,tr,' +\n  'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +\n  'output,progress,select,textarea,' +\n  'details,dialog,menu,menuitem,summary,' +\n  'content,element,shadow,template,blockquote,iframe,tfoot'\n);\n\n// this map is intentionally selective, only covering SVG elements that may\n// contain child elements.\nvar isSVG = makeMap(\n  'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +\n  'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +\n  'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',\n  true\n);\n\nvar isPreTag = function (tag) { return tag === 'pre'; };\n\nvar isReservedTag = function (tag) {\n  return isHTMLTag(tag) || isSVG(tag)\n};\n\nfunction getTagNamespace (tag) {\n  if (isSVG(tag)) {\n    return 'svg'\n  }\n  // basic support for MathML\n  // note it doesn't support other MathML elements being component roots\n  if (tag === 'math') {\n    return 'math'\n  }\n}\n\nvar unknownElementCache = Object.create(null);\nfunction isUnknownElement (tag) {\n  /* istanbul ignore if */\n  if (!inBrowser) {\n    return true\n  }\n  if (isReservedTag(tag)) {\n    return false\n  }\n  tag = tag.toLowerCase();\n  /* istanbul ignore if */\n  if (unknownElementCache[tag] != null) {\n    return unknownElementCache[tag]\n  }\n  var el = document.createElement(tag);\n  if (tag.indexOf('-') > -1) {\n    // http://stackoverflow.com/a/28210364/1070244\n    return (unknownElementCache[tag] = (\n      el.constructor === window.HTMLUnknownElement ||\n      el.constructor === window.HTMLElement\n    ))\n  } else {\n    return (unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()))\n  }\n}\n\nvar isTextInputType = makeMap('text,number,password,search,email,tel,url');\n\n/*  */\n\n/**\n * Query an element selector if it's not an element already.\n */\nfunction query (el) {\n  if (typeof el === 'string') {\n    var selected = document.querySelector(el);\n    if (!selected) {\n      \"development\" !== 'production' && warn(\n        'Cannot find element: ' + el\n      );\n      return document.createElement('div')\n    }\n    return selected\n  } else {\n    return el\n  }\n}\n\n/*  */\n\nfunction createElement$1 (tagName, vnode) {\n  var elm = document.createElement(tagName);\n  if (tagName !== 'select') {\n    return elm\n  }\n  // false or null will remove the attribute but undefined will not\n  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {\n    elm.setAttribute('multiple', 'multiple');\n  }\n  return elm\n}\n\nfunction createElementNS (namespace, tagName) {\n  return document.createElementNS(namespaceMap[namespace], tagName)\n}\n\nfunction createTextNode (text) {\n  return document.createTextNode(text)\n}\n\nfunction createComment (text) {\n  return document.createComment(text)\n}\n\nfunction insertBefore (parentNode, newNode, referenceNode) {\n  parentNode.insertBefore(newNode, referenceNode);\n}\n\nfunction removeChild (node, child) {\n  node.removeChild(child);\n}\n\nfunction appendChild (node, child) {\n  node.appendChild(child);\n}\n\nfunction parentNode (node) {\n  return node.parentNode\n}\n\nfunction nextSibling (node) {\n  return node.nextSibling\n}\n\nfunction tagName (node) {\n  return node.tagName\n}\n\nfunction setTextContent (node, text) {\n  node.textContent = text;\n}\n\nfunction setAttribute (node, key, val) {\n  node.setAttribute(key, val);\n}\n\n\nvar nodeOps = Object.freeze({\n\tcreateElement: createElement$1,\n\tcreateElementNS: createElementNS,\n\tcreateTextNode: createTextNode,\n\tcreateComment: createComment,\n\tinsertBefore: insertBefore,\n\tremoveChild: removeChild,\n\tappendChild: appendChild,\n\tparentNode: parentNode,\n\tnextSibling: nextSibling,\n\ttagName: tagName,\n\tsetTextContent: setTextContent,\n\tsetAttribute: setAttribute\n});\n\n/*  */\n\nvar ref = {\n  create: function create (_, vnode) {\n    registerRef(vnode);\n  },\n  update: function update (oldVnode, vnode) {\n    if (oldVnode.data.ref !== vnode.data.ref) {\n      registerRef(oldVnode, true);\n      registerRef(vnode);\n    }\n  },\n  destroy: function destroy (vnode) {\n    registerRef(vnode, true);\n  }\n};\n\nfunction registerRef (vnode, isRemoval) {\n  var key = vnode.data.ref;\n  if (!key) { return }\n\n  var vm = vnode.context;\n  var ref = vnode.componentInstance || vnode.elm;\n  var refs = vm.$refs;\n  if (isRemoval) {\n    if (Array.isArray(refs[key])) {\n      remove(refs[key], ref);\n    } else if (refs[key] === ref) {\n      refs[key] = undefined;\n    }\n  } else {\n    if (vnode.data.refInFor) {\n      if (!Array.isArray(refs[key])) {\n        refs[key] = [ref];\n      } else if (refs[key].indexOf(ref) < 0) {\n        // $flow-disable-line\n        refs[key].push(ref);\n      }\n    } else {\n      refs[key] = ref;\n    }\n  }\n}\n\n/**\n * Virtual DOM patching algorithm based on Snabbdom by\n * Simon Friis Vindum (@paldepind)\n * Licensed under the MIT License\n * https://github.com/paldepind/snabbdom/blob/master/LICENSE\n *\n * modified by Evan You (@yyx990803)\n *\n * Not type-checking this because this file is perf-critical and the cost\n * of making flow understand it is not worth it.\n */\n\nvar emptyNode = new VNode('', {}, []);\n\nvar hooks = ['create', 'activate', 'update', 'remove', 'destroy'];\n\nfunction sameVnode (a, b) {\n  return (\n    a.key === b.key && (\n      (\n        a.tag === b.tag &&\n        a.isComment === b.isComment &&\n        isDef(a.data) === isDef(b.data) &&\n        sameInputType(a, b)\n      ) || (\n        isTrue(a.isAsyncPlaceholder) &&\n        a.asyncFactory === b.asyncFactory &&\n        isUndef(b.asyncFactory.error)\n      )\n    )\n  )\n}\n\nfunction sameInputType (a, b) {\n  if (a.tag !== 'input') { return true }\n  var i;\n  var typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type;\n  var typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type;\n  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)\n}\n\nfunction createKeyToOldIdx (children, beginIdx, endIdx) {\n  var i, key;\n  var map = {};\n  for (i = beginIdx; i <= endIdx; ++i) {\n    key = children[i].key;\n    if (isDef(key)) { map[key] = i; }\n  }\n  return map\n}\n\nfunction createPatchFunction (backend) {\n  var i, j;\n  var cbs = {};\n\n  var modules = backend.modules;\n  var nodeOps = backend.nodeOps;\n\n  for (i = 0; i < hooks.length; ++i) {\n    cbs[hooks[i]] = [];\n    for (j = 0; j < modules.length; ++j) {\n      if (isDef(modules[j][hooks[i]])) {\n        cbs[hooks[i]].push(modules[j][hooks[i]]);\n      }\n    }\n  }\n\n  function emptyNodeAt (elm) {\n    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)\n  }\n\n  function createRmCb (childElm, listeners) {\n    function remove () {\n      if (--remove.listeners === 0) {\n        removeNode(childElm);\n      }\n    }\n    remove.listeners = listeners;\n    return remove\n  }\n\n  function removeNode (el) {\n    var parent = nodeOps.parentNode(el);\n    // element may have already been removed due to v-html / v-text\n    if (isDef(parent)) {\n      nodeOps.removeChild(parent, el);\n    }\n  }\n\n  function isUnknownElement$$1 (vnode, inVPre) {\n    return (\n      !inVPre &&\n      !vnode.ns &&\n      !(\n        config.ignoredElements.length &&\n        config.ignoredElements.some(function (ignore) {\n          return isRegExp(ignore)\n            ? ignore.test(vnode.tag)\n            : ignore === vnode.tag\n        })\n      ) &&\n      config.isUnknownElement(vnode.tag)\n    )\n  }\n\n  var creatingElmInVPre = 0;\n  function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {\n    vnode.isRootInsert = !nested; // for transition enter check\n    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {\n      return\n    }\n\n    var data = vnode.data;\n    var children = vnode.children;\n    var tag = vnode.tag;\n    if (isDef(tag)) {\n      {\n        if (data && data.pre) {\n          creatingElmInVPre++;\n        }\n        if (isUnknownElement$$1(vnode, creatingElmInVPre)) {\n          warn(\n            'Unknown custom element: <' + tag + '> - did you ' +\n            'register the component correctly? For recursive components, ' +\n            'make sure to provide the \"name\" option.',\n            vnode.context\n          );\n        }\n      }\n      vnode.elm = vnode.ns\n        ? nodeOps.createElementNS(vnode.ns, tag)\n        : nodeOps.createElement(tag, vnode);\n      setScope(vnode);\n\n      /* istanbul ignore if */\n      {\n        createChildren(vnode, children, insertedVnodeQueue);\n        if (isDef(data)) {\n          invokeCreateHooks(vnode, insertedVnodeQueue);\n        }\n        insert(parentElm, vnode.elm, refElm);\n      }\n\n      if (\"development\" !== 'production' && data && data.pre) {\n        creatingElmInVPre--;\n      }\n    } else if (isTrue(vnode.isComment)) {\n      vnode.elm = nodeOps.createComment(vnode.text);\n      insert(parentElm, vnode.elm, refElm);\n    } else {\n      vnode.elm = nodeOps.createTextNode(vnode.text);\n      insert(parentElm, vnode.elm, refElm);\n    }\n  }\n\n  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {\n    var i = vnode.data;\n    if (isDef(i)) {\n      var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;\n      if (isDef(i = i.hook) && isDef(i = i.init)) {\n        i(vnode, false /* hydrating */, parentElm, refElm);\n      }\n      // after calling the init hook, if the vnode is a child component\n      // it should've created a child instance and mounted it. the child\n      // component also has set the placeholder vnode's elm.\n      // in that case we can just return the element and be done.\n      if (isDef(vnode.componentInstance)) {\n        initComponent(vnode, insertedVnodeQueue);\n        if (isTrue(isReactivated)) {\n          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);\n        }\n        return true\n      }\n    }\n  }\n\n  function initComponent (vnode, insertedVnodeQueue) {\n    if (isDef(vnode.data.pendingInsert)) {\n      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);\n      vnode.data.pendingInsert = null;\n    }\n    vnode.elm = vnode.componentInstance.$el;\n    if (isPatchable(vnode)) {\n      invokeCreateHooks(vnode, insertedVnodeQueue);\n      setScope(vnode);\n    } else {\n      // empty component root.\n      // skip all element-related modules except for ref (#3455)\n      registerRef(vnode);\n      // make sure to invoke the insert hook\n      insertedVnodeQueue.push(vnode);\n    }\n  }\n\n  function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {\n    var i;\n    // hack for #4339: a reactivated component with inner transition\n    // does not trigger because the inner node's created hooks are not called\n    // again. It's not ideal to involve module-specific logic in here but\n    // there doesn't seem to be a better way to do it.\n    var innerNode = vnode;\n    while (innerNode.componentInstance) {\n      innerNode = innerNode.componentInstance._vnode;\n      if (isDef(i = innerNode.data) && isDef(i = i.transition)) {\n        for (i = 0; i < cbs.activate.length; ++i) {\n          cbs.activate[i](emptyNode, innerNode);\n        }\n        insertedVnodeQueue.push(innerNode);\n        break\n      }\n    }\n    // unlike a newly created component,\n    // a reactivated keep-alive component doesn't insert itself\n    insert(parentElm, vnode.elm, refElm);\n  }\n\n  function insert (parent, elm, ref$$1) {\n    if (isDef(parent)) {\n      if (isDef(ref$$1)) {\n        if (ref$$1.parentNode === parent) {\n          nodeOps.insertBefore(parent, elm, ref$$1);\n        }\n      } else {\n        nodeOps.appendChild(parent, elm);\n      }\n    }\n  }\n\n  function createChildren (vnode, children, insertedVnodeQueue) {\n    if (Array.isArray(children)) {\n      {\n        checkDuplicateKeys(children);\n      }\n      for (var i = 0; i < children.length; ++i) {\n        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true);\n      }\n    } else if (isPrimitive(vnode.text)) {\n      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));\n    }\n  }\n\n  function isPatchable (vnode) {\n    while (vnode.componentInstance) {\n      vnode = vnode.componentInstance._vnode;\n    }\n    return isDef(vnode.tag)\n  }\n\n  function invokeCreateHooks (vnode, insertedVnodeQueue) {\n    for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {\n      cbs.create[i$1](emptyNode, vnode);\n    }\n    i = vnode.data.hook; // Reuse variable\n    if (isDef(i)) {\n      if (isDef(i.create)) { i.create(emptyNode, vnode); }\n      if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }\n    }\n  }\n\n  // set scope id attribute for scoped CSS.\n  // this is implemented as a special case to avoid the overhead\n  // of going through the normal attribute patching process.\n  function setScope (vnode) {\n    var i;\n    if (isDef(i = vnode.fnScopeId)) {\n      nodeOps.setAttribute(vnode.elm, i, '');\n    } else {\n      var ancestor = vnode;\n      while (ancestor) {\n        if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {\n          nodeOps.setAttribute(vnode.elm, i, '');\n        }\n        ancestor = ancestor.parent;\n      }\n    }\n    // for slot content they should also get the scopeId from the host instance.\n    if (isDef(i = activeInstance) &&\n      i !== vnode.context &&\n      i !== vnode.fnContext &&\n      isDef(i = i.$options._scopeId)\n    ) {\n      nodeOps.setAttribute(vnode.elm, i, '');\n    }\n  }\n\n  function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {\n    for (; startIdx <= endIdx; ++startIdx) {\n      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm);\n    }\n  }\n\n  function invokeDestroyHook (vnode) {\n    var i, j;\n    var data = vnode.data;\n    if (isDef(data)) {\n      if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }\n      for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }\n    }\n    if (isDef(i = vnode.children)) {\n      for (j = 0; j < vnode.children.length; ++j) {\n        invokeDestroyHook(vnode.children[j]);\n      }\n    }\n  }\n\n  function removeVnodes (parentElm, vnodes, startIdx, endIdx) {\n    for (; startIdx <= endIdx; ++startIdx) {\n      var ch = vnodes[startIdx];\n      if (isDef(ch)) {\n        if (isDef(ch.tag)) {\n          removeAndInvokeRemoveHook(ch);\n          invokeDestroyHook(ch);\n        } else { // Text node\n          removeNode(ch.elm);\n        }\n      }\n    }\n  }\n\n  function removeAndInvokeRemoveHook (vnode, rm) {\n    if (isDef(rm) || isDef(vnode.data)) {\n      var i;\n      var listeners = cbs.remove.length + 1;\n      if (isDef(rm)) {\n        // we have a recursively passed down rm callback\n        // increase the listeners count\n        rm.listeners += listeners;\n      } else {\n        // directly removing\n        rm = createRmCb(vnode.elm, listeners);\n      }\n      // recursively invoke hooks on child component root node\n      if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {\n        removeAndInvokeRemoveHook(i, rm);\n      }\n      for (i = 0; i < cbs.remove.length; ++i) {\n        cbs.remove[i](vnode, rm);\n      }\n      if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {\n        i(vnode, rm);\n      } else {\n        rm();\n      }\n    } else {\n      removeNode(vnode.elm);\n    }\n  }\n\n  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {\n    var oldStartIdx = 0;\n    var newStartIdx = 0;\n    var oldEndIdx = oldCh.length - 1;\n    var oldStartVnode = oldCh[0];\n    var oldEndVnode = oldCh[oldEndIdx];\n    var newEndIdx = newCh.length - 1;\n    var newStartVnode = newCh[0];\n    var newEndVnode = newCh[newEndIdx];\n    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;\n\n    // removeOnly is a special flag used only by <transition-group>\n    // to ensure removed elements stay in correct relative positions\n    // during leaving transitions\n    var canMove = !removeOnly;\n\n    {\n      checkDuplicateKeys(newCh);\n    }\n\n    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {\n      if (isUndef(oldStartVnode)) {\n        oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left\n      } else if (isUndef(oldEndVnode)) {\n        oldEndVnode = oldCh[--oldEndIdx];\n      } else if (sameVnode(oldStartVnode, newStartVnode)) {\n        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);\n        oldStartVnode = oldCh[++oldStartIdx];\n        newStartVnode = newCh[++newStartIdx];\n      } else if (sameVnode(oldEndVnode, newEndVnode)) {\n        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);\n        oldEndVnode = oldCh[--oldEndIdx];\n        newEndVnode = newCh[--newEndIdx];\n      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right\n        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);\n        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));\n        oldStartVnode = oldCh[++oldStartIdx];\n        newEndVnode = newCh[--newEndIdx];\n      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left\n        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);\n        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);\n        oldEndVnode = oldCh[--oldEndIdx];\n        newStartVnode = newCh[++newStartIdx];\n      } else {\n        if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }\n        idxInOld = isDef(newStartVnode.key)\n          ? oldKeyToIdx[newStartVnode.key]\n          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);\n        if (isUndef(idxInOld)) { // New element\n          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm);\n        } else {\n          vnodeToMove = oldCh[idxInOld];\n          if (sameVnode(vnodeToMove, newStartVnode)) {\n            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);\n            oldCh[idxInOld] = undefined;\n            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);\n          } else {\n            // same key but different element. treat as new element\n            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm);\n          }\n        }\n        newStartVnode = newCh[++newStartIdx];\n      }\n    }\n    if (oldStartIdx > oldEndIdx) {\n      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;\n      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);\n    } else if (newStartIdx > newEndIdx) {\n      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);\n    }\n  }\n\n  function checkDuplicateKeys (children) {\n    var seenKeys = {};\n    for (var i = 0; i < children.length; i++) {\n      var vnode = children[i];\n      var key = vnode.key;\n      if (isDef(key)) {\n        if (seenKeys[key]) {\n          warn(\n            (\"Duplicate keys detected: '\" + key + \"'. This may cause an update error.\"),\n            vnode.context\n          );\n        } else {\n          seenKeys[key] = true;\n        }\n      }\n    }\n  }\n\n  function findIdxInOld (node, oldCh, start, end) {\n    for (var i = start; i < end; i++) {\n      var c = oldCh[i];\n      if (isDef(c) && sameVnode(node, c)) { return i }\n    }\n  }\n\n  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {\n    if (oldVnode === vnode) {\n      return\n    }\n\n    var elm = vnode.elm = oldVnode.elm;\n\n    if (isTrue(oldVnode.isAsyncPlaceholder)) {\n      if (isDef(vnode.asyncFactory.resolved)) {\n        hydrate(oldVnode.elm, vnode, insertedVnodeQueue);\n      } else {\n        vnode.isAsyncPlaceholder = true;\n      }\n      return\n    }\n\n    // reuse element for static trees.\n    // note we only do this if the vnode is cloned -\n    // if the new node is not cloned it means the render functions have been\n    // reset by the hot-reload-api and we need to do a proper re-render.\n    if (isTrue(vnode.isStatic) &&\n      isTrue(oldVnode.isStatic) &&\n      vnode.key === oldVnode.key &&\n      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))\n    ) {\n      vnode.componentInstance = oldVnode.componentInstance;\n      return\n    }\n\n    var i;\n    var data = vnode.data;\n    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {\n      i(oldVnode, vnode);\n    }\n\n    var oldCh = oldVnode.children;\n    var ch = vnode.children;\n    if (isDef(data) && isPatchable(vnode)) {\n      for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }\n      if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }\n    }\n    if (isUndef(vnode.text)) {\n      if (isDef(oldCh) && isDef(ch)) {\n        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }\n      } else if (isDef(ch)) {\n        if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }\n        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);\n      } else if (isDef(oldCh)) {\n        removeVnodes(elm, oldCh, 0, oldCh.length - 1);\n      } else if (isDef(oldVnode.text)) {\n        nodeOps.setTextContent(elm, '');\n      }\n    } else if (oldVnode.text !== vnode.text) {\n      nodeOps.setTextContent(elm, vnode.text);\n    }\n    if (isDef(data)) {\n      if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }\n    }\n  }\n\n  function invokeInsertHook (vnode, queue, initial) {\n    // delay insert hooks for component root nodes, invoke them after the\n    // element is really inserted\n    if (isTrue(initial) && isDef(vnode.parent)) {\n      vnode.parent.data.pendingInsert = queue;\n    } else {\n      for (var i = 0; i < queue.length; ++i) {\n        queue[i].data.hook.insert(queue[i]);\n      }\n    }\n  }\n\n  var hydrationBailed = false;\n  // list of modules that can skip create hook during hydration because they\n  // are already rendered on the client or has no need for initialization\n  // Note: style is excluded because it relies on initial clone for future\n  // deep updates (#7063).\n  var isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key');\n\n  // Note: this is a browser-only function so we can assume elms are DOM nodes.\n  function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {\n    var i;\n    var tag = vnode.tag;\n    var data = vnode.data;\n    var children = vnode.children;\n    inVPre = inVPre || (data && data.pre);\n    vnode.elm = elm;\n\n    if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {\n      vnode.isAsyncPlaceholder = true;\n      return true\n    }\n    // assert node match\n    {\n      if (!assertNodeMatch(elm, vnode, inVPre)) {\n        return false\n      }\n    }\n    if (isDef(data)) {\n      if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */); }\n      if (isDef(i = vnode.componentInstance)) {\n        // child component. it should have hydrated its own tree.\n        initComponent(vnode, insertedVnodeQueue);\n        return true\n      }\n    }\n    if (isDef(tag)) {\n      if (isDef(children)) {\n        // empty element, allow client to pick up and populate children\n        if (!elm.hasChildNodes()) {\n          createChildren(vnode, children, insertedVnodeQueue);\n        } else {\n          // v-html and domProps: innerHTML\n          if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {\n            if (i !== elm.innerHTML) {\n              /* istanbul ignore if */\n              if (\"development\" !== 'production' &&\n                typeof console !== 'undefined' &&\n                !hydrationBailed\n              ) {\n                hydrationBailed = true;\n                console.warn('Parent: ', elm);\n                console.warn('server innerHTML: ', i);\n                console.warn('client innerHTML: ', elm.innerHTML);\n              }\n              return false\n            }\n          } else {\n            // iterate and compare children lists\n            var childrenMatch = true;\n            var childNode = elm.firstChild;\n            for (var i$1 = 0; i$1 < children.length; i$1++) {\n              if (!childNode || !hydrate(childNode, children[i$1], insertedVnodeQueue, inVPre)) {\n                childrenMatch = false;\n                break\n              }\n              childNode = childNode.nextSibling;\n            }\n            // if childNode is not null, it means the actual childNodes list is\n            // longer than the virtual children list.\n            if (!childrenMatch || childNode) {\n              /* istanbul ignore if */\n              if (\"development\" !== 'production' &&\n                typeof console !== 'undefined' &&\n                !hydrationBailed\n              ) {\n                hydrationBailed = true;\n                console.warn('Parent: ', elm);\n                console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children);\n              }\n              return false\n            }\n          }\n        }\n      }\n      if (isDef(data)) {\n        var fullInvoke = false;\n        for (var key in data) {\n          if (!isRenderedModule(key)) {\n            fullInvoke = true;\n            invokeCreateHooks(vnode, insertedVnodeQueue);\n            break\n          }\n        }\n        if (!fullInvoke && data['class']) {\n          // ensure collecting deps for deep class bindings for future updates\n          traverse(data['class']);\n        }\n      }\n    } else if (elm.data !== vnode.text) {\n      elm.data = vnode.text;\n    }\n    return true\n  }\n\n  function assertNodeMatch (node, vnode, inVPre) {\n    if (isDef(vnode.tag)) {\n      return vnode.tag.indexOf('vue-component') === 0 || (\n        !isUnknownElement$$1(vnode, inVPre) &&\n        vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase())\n      )\n    } else {\n      return node.nodeType === (vnode.isComment ? 8 : 3)\n    }\n  }\n\n  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {\n    if (isUndef(vnode)) {\n      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }\n      return\n    }\n\n    var isInitialPatch = false;\n    var insertedVnodeQueue = [];\n\n    if (isUndef(oldVnode)) {\n      // empty mount (likely as component), create new root element\n      isInitialPatch = true;\n      createElm(vnode, insertedVnodeQueue, parentElm, refElm);\n    } else {\n      var isRealElement = isDef(oldVnode.nodeType);\n      if (!isRealElement && sameVnode(oldVnode, vnode)) {\n        // patch existing root node\n        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);\n      } else {\n        if (isRealElement) {\n          // mounting to a real element\n          // check if this is server-rendered content and if we can perform\n          // a successful hydration.\n          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {\n            oldVnode.removeAttribute(SSR_ATTR);\n            hydrating = true;\n          }\n          if (isTrue(hydrating)) {\n            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {\n              invokeInsertHook(vnode, insertedVnodeQueue, true);\n              return oldVnode\n            } else {\n              warn(\n                'The client-side rendered virtual DOM tree is not matching ' +\n                'server-rendered content. This is likely caused by incorrect ' +\n                'HTML markup, for example nesting block-level elements inside ' +\n                '<p>, or missing <tbody>. Bailing hydration and performing ' +\n                'full client-side render.'\n              );\n            }\n          }\n          // either not server-rendered, or hydration failed.\n          // create an empty node and replace it\n          oldVnode = emptyNodeAt(oldVnode);\n        }\n\n        // replacing existing element\n        var oldElm = oldVnode.elm;\n        var parentElm$1 = nodeOps.parentNode(oldElm);\n\n        // create new node\n        createElm(\n          vnode,\n          insertedVnodeQueue,\n          // extremely rare edge case: do not insert if old element is in a\n          // leaving transition. Only happens when combining transition +\n          // keep-alive + HOCs. (#4590)\n          oldElm._leaveCb ? null : parentElm$1,\n          nodeOps.nextSibling(oldElm)\n        );\n\n        // update parent placeholder node element, recursively\n        if (isDef(vnode.parent)) {\n          var ancestor = vnode.parent;\n          var patchable = isPatchable(vnode);\n          while (ancestor) {\n            for (var i = 0; i < cbs.destroy.length; ++i) {\n              cbs.destroy[i](ancestor);\n            }\n            ancestor.elm = vnode.elm;\n            if (patchable) {\n              for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {\n                cbs.create[i$1](emptyNode, ancestor);\n              }\n              // #6513\n              // invoke insert hooks that may have been merged by create hooks.\n              // e.g. for directives that uses the \"inserted\" hook.\n              var insert = ancestor.data.hook.insert;\n              if (insert.merged) {\n                // start at index 1 to avoid re-invoking component mounted hook\n                for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {\n                  insert.fns[i$2]();\n                }\n              }\n            } else {\n              registerRef(ancestor);\n            }\n            ancestor = ancestor.parent;\n          }\n        }\n\n        // destroy old node\n        if (isDef(parentElm$1)) {\n          removeVnodes(parentElm$1, [oldVnode], 0, 0);\n        } else if (isDef(oldVnode.tag)) {\n          invokeDestroyHook(oldVnode);\n        }\n      }\n    }\n\n    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);\n    return vnode.elm\n  }\n}\n\n/*  */\n\nvar directives = {\n  create: updateDirectives,\n  update: updateDirectives,\n  destroy: function unbindDirectives (vnode) {\n    updateDirectives(vnode, emptyNode);\n  }\n};\n\nfunction updateDirectives (oldVnode, vnode) {\n  if (oldVnode.data.directives || vnode.data.directives) {\n    _update(oldVnode, vnode);\n  }\n}\n\nfunction _update (oldVnode, vnode) {\n  var isCreate = oldVnode === emptyNode;\n  var isDestroy = vnode === emptyNode;\n  var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);\n  var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);\n\n  var dirsWithInsert = [];\n  var dirsWithPostpatch = [];\n\n  var key, oldDir, dir;\n  for (key in newDirs) {\n    oldDir = oldDirs[key];\n    dir = newDirs[key];\n    if (!oldDir) {\n      // new directive, bind\n      callHook$1(dir, 'bind', vnode, oldVnode);\n      if (dir.def && dir.def.inserted) {\n        dirsWithInsert.push(dir);\n      }\n    } else {\n      // existing directive, update\n      dir.oldValue = oldDir.value;\n      callHook$1(dir, 'update', vnode, oldVnode);\n      if (dir.def && dir.def.componentUpdated) {\n        dirsWithPostpatch.push(dir);\n      }\n    }\n  }\n\n  if (dirsWithInsert.length) {\n    var callInsert = function () {\n      for (var i = 0; i < dirsWithInsert.length; i++) {\n        callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);\n      }\n    };\n    if (isCreate) {\n      mergeVNodeHook(vnode, 'insert', callInsert);\n    } else {\n      callInsert();\n    }\n  }\n\n  if (dirsWithPostpatch.length) {\n    mergeVNodeHook(vnode, 'postpatch', function () {\n      for (var i = 0; i < dirsWithPostpatch.length; i++) {\n        callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);\n      }\n    });\n  }\n\n  if (!isCreate) {\n    for (key in oldDirs) {\n      if (!newDirs[key]) {\n        // no longer present, unbind\n        callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);\n      }\n    }\n  }\n}\n\nvar emptyModifiers = Object.create(null);\n\nfunction normalizeDirectives$1 (\n  dirs,\n  vm\n) {\n  var res = Object.create(null);\n  if (!dirs) {\n    return res\n  }\n  var i, dir;\n  for (i = 0; i < dirs.length; i++) {\n    dir = dirs[i];\n    if (!dir.modifiers) {\n      dir.modifiers = emptyModifiers;\n    }\n    res[getRawDirName(dir)] = dir;\n    dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);\n  }\n  return res\n}\n\nfunction getRawDirName (dir) {\n  return dir.rawName || ((dir.name) + \".\" + (Object.keys(dir.modifiers || {}).join('.')))\n}\n\nfunction callHook$1 (dir, hook, vnode, oldVnode, isDestroy) {\n  var fn = dir.def && dir.def[hook];\n  if (fn) {\n    try {\n      fn(vnode.elm, dir, vnode, oldVnode, isDestroy);\n    } catch (e) {\n      handleError(e, vnode.context, (\"directive \" + (dir.name) + \" \" + hook + \" hook\"));\n    }\n  }\n}\n\nvar baseModules = [\n  ref,\n  directives\n];\n\n/*  */\n\nfunction updateAttrs (oldVnode, vnode) {\n  var opts = vnode.componentOptions;\n  if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {\n    return\n  }\n  if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {\n    return\n  }\n  var key, cur, old;\n  var elm = vnode.elm;\n  var oldAttrs = oldVnode.data.attrs || {};\n  var attrs = vnode.data.attrs || {};\n  // clone observed objects, as the user probably wants to mutate it\n  if (isDef(attrs.__ob__)) {\n    attrs = vnode.data.attrs = extend({}, attrs);\n  }\n\n  for (key in attrs) {\n    cur = attrs[key];\n    old = oldAttrs[key];\n    if (old !== cur) {\n      setAttr(elm, key, cur);\n    }\n  }\n  // #4391: in IE9, setting type can reset value for input[type=radio]\n  // #6666: IE/Edge forces progress value down to 1 before setting a max\n  /* istanbul ignore if */\n  if ((isIE || isEdge) && attrs.value !== oldAttrs.value) {\n    setAttr(elm, 'value', attrs.value);\n  }\n  for (key in oldAttrs) {\n    if (isUndef(attrs[key])) {\n      if (isXlink(key)) {\n        elm.removeAttributeNS(xlinkNS, getXlinkProp(key));\n      } else if (!isEnumeratedAttr(key)) {\n        elm.removeAttribute(key);\n      }\n    }\n  }\n}\n\nfunction setAttr (el, key, value) {\n  if (isBooleanAttr(key)) {\n    // set attribute for blank value\n    // e.g. <option disabled>Select one</option>\n    if (isFalsyAttrValue(value)) {\n      el.removeAttribute(key);\n    } else {\n      // technically allowfullscreen is a boolean attribute for <iframe>,\n      // but Flash expects a value of \"true\" when used on <embed> tag\n      value = key === 'allowfullscreen' && el.tagName === 'EMBED'\n        ? 'true'\n        : key;\n      el.setAttribute(key, value);\n    }\n  } else if (isEnumeratedAttr(key)) {\n    el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true');\n  } else if (isXlink(key)) {\n    if (isFalsyAttrValue(value)) {\n      el.removeAttributeNS(xlinkNS, getXlinkProp(key));\n    } else {\n      el.setAttributeNS(xlinkNS, key, value);\n    }\n  } else {\n    if (isFalsyAttrValue(value)) {\n      el.removeAttribute(key);\n    } else {\n      // #7138: IE10 & 11 fires input event when setting placeholder on\n      // <textarea>... block the first input event and remove the blocker\n      // immediately.\n      /* istanbul ignore if */\n      if (\n        isIE && !isIE9 &&\n        el.tagName === 'TEXTAREA' &&\n        key === 'placeholder' && !el.__ieph\n      ) {\n        var blocker = function (e) {\n          e.stopImmediatePropagation();\n          el.removeEventListener('input', blocker);\n        };\n        el.addEventListener('input', blocker);\n        // $flow-disable-line\n        el.__ieph = true; /* IE placeholder patched */\n      }\n      el.setAttribute(key, value);\n    }\n  }\n}\n\nvar attrs = {\n  create: updateAttrs,\n  update: updateAttrs\n};\n\n/*  */\n\nfunction updateClass (oldVnode, vnode) {\n  var el = vnode.elm;\n  var data = vnode.data;\n  var oldData = oldVnode.data;\n  if (\n    isUndef(data.staticClass) &&\n    isUndef(data.class) && (\n      isUndef(oldData) || (\n        isUndef(oldData.staticClass) &&\n        isUndef(oldData.class)\n      )\n    )\n  ) {\n    return\n  }\n\n  var cls = genClassForVnode(vnode);\n\n  // handle transition classes\n  var transitionClass = el._transitionClasses;\n  if (isDef(transitionClass)) {\n    cls = concat(cls, stringifyClass(transitionClass));\n  }\n\n  // set the class\n  if (cls !== el._prevClass) {\n    el.setAttribute('class', cls);\n    el._prevClass = cls;\n  }\n}\n\nvar klass = {\n  create: updateClass,\n  update: updateClass\n};\n\n/*  */\n\nvar validDivisionCharRE = /[\\w).+\\-_$\\]]/;\n\nfunction parseFilters (exp) {\n  var inSingle = false;\n  var inDouble = false;\n  var inTemplateString = false;\n  var inRegex = false;\n  var curly = 0;\n  var square = 0;\n  var paren = 0;\n  var lastFilterIndex = 0;\n  var c, prev, i, expression, filters;\n\n  for (i = 0; i < exp.length; i++) {\n    prev = c;\n    c = exp.charCodeAt(i);\n    if (inSingle) {\n      if (c === 0x27 && prev !== 0x5C) { inSingle = false; }\n    } else if (inDouble) {\n      if (c === 0x22 && prev !== 0x5C) { inDouble = false; }\n    } else if (inTemplateString) {\n      if (c === 0x60 && prev !== 0x5C) { inTemplateString = false; }\n    } else if (inRegex) {\n      if (c === 0x2f && prev !== 0x5C) { inRegex = false; }\n    } else if (\n      c === 0x7C && // pipe\n      exp.charCodeAt(i + 1) !== 0x7C &&\n      exp.charCodeAt(i - 1) !== 0x7C &&\n      !curly && !square && !paren\n    ) {\n      if (expression === undefined) {\n        // first filter, end of expression\n        lastFilterIndex = i + 1;\n        expression = exp.slice(0, i).trim();\n      } else {\n        pushFilter();\n      }\n    } else {\n      switch (c) {\n        case 0x22: inDouble = true; break         // \"\n        case 0x27: inSingle = true; break         // '\n        case 0x60: inTemplateString = true; break // `\n        case 0x28: paren++; break                 // (\n        case 0x29: paren--; break                 // )\n        case 0x5B: square++; break                // [\n        case 0x5D: square--; break                // ]\n        case 0x7B: curly++; break                 // {\n        case 0x7D: curly--; break                 // }\n      }\n      if (c === 0x2f) { // /\n        var j = i - 1;\n        var p = (void 0);\n        // find first non-whitespace prev char\n        for (; j >= 0; j--) {\n          p = exp.charAt(j);\n          if (p !== ' ') { break }\n        }\n        if (!p || !validDivisionCharRE.test(p)) {\n          inRegex = true;\n        }\n      }\n    }\n  }\n\n  if (expression === undefined) {\n    expression = exp.slice(0, i).trim();\n  } else if (lastFilterIndex !== 0) {\n    pushFilter();\n  }\n\n  function pushFilter () {\n    (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());\n    lastFilterIndex = i + 1;\n  }\n\n  if (filters) {\n    for (i = 0; i < filters.length; i++) {\n      expression = wrapFilter(expression, filters[i]);\n    }\n  }\n\n  return expression\n}\n\nfunction wrapFilter (exp, filter) {\n  var i = filter.indexOf('(');\n  if (i < 0) {\n    // _f: resolveFilter\n    return (\"_f(\\\"\" + filter + \"\\\")(\" + exp + \")\")\n  } else {\n    var name = filter.slice(0, i);\n    var args = filter.slice(i + 1);\n    return (\"_f(\\\"\" + name + \"\\\")(\" + exp + \",\" + args)\n  }\n}\n\n/*  */\n\nfunction baseWarn (msg) {\n  console.error((\"[Vue compiler]: \" + msg));\n}\n\nfunction pluckModuleFunction (\n  modules,\n  key\n) {\n  return modules\n    ? modules.map(function (m) { return m[key]; }).filter(function (_) { return _; })\n    : []\n}\n\nfunction addProp (el, name, value) {\n  (el.props || (el.props = [])).push({ name: name, value: value });\n}\n\nfunction addAttr (el, name, value) {\n  (el.attrs || (el.attrs = [])).push({ name: name, value: value });\n}\n\nfunction addDirective (\n  el,\n  name,\n  rawName,\n  value,\n  arg,\n  modifiers\n) {\n  (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });\n}\n\nfunction addHandler (\n  el,\n  name,\n  value,\n  modifiers,\n  important,\n  warn\n) {\n  modifiers = modifiers || emptyObject;\n  // warn prevent and passive modifier\n  /* istanbul ignore if */\n  if (\n    \"development\" !== 'production' && warn &&\n    modifiers.prevent && modifiers.passive\n  ) {\n    warn(\n      'passive and prevent can\\'t be used together. ' +\n      'Passive handler can\\'t prevent default event.'\n    );\n  }\n\n  // check capture modifier\n  if (modifiers.capture) {\n    delete modifiers.capture;\n    name = '!' + name; // mark the event as captured\n  }\n  if (modifiers.once) {\n    delete modifiers.once;\n    name = '~' + name; // mark the event as once\n  }\n  /* istanbul ignore if */\n  if (modifiers.passive) {\n    delete modifiers.passive;\n    name = '&' + name; // mark the event as passive\n  }\n\n  // normalize click.right and click.middle since they don't actually fire\n  // this is technically browser-specific, but at least for now browsers are\n  // the only target envs that have right/middle clicks.\n  if (name === 'click') {\n    if (modifiers.right) {\n      name = 'contextmenu';\n      delete modifiers.right;\n    } else if (modifiers.middle) {\n      name = 'mouseup';\n    }\n  }\n\n  var events;\n  if (modifiers.native) {\n    delete modifiers.native;\n    events = el.nativeEvents || (el.nativeEvents = {});\n  } else {\n    events = el.events || (el.events = {});\n  }\n\n  var newHandler = { value: value };\n  if (modifiers !== emptyObject) {\n    newHandler.modifiers = modifiers;\n  }\n\n  var handlers = events[name];\n  /* istanbul ignore if */\n  if (Array.isArray(handlers)) {\n    important ? handlers.unshift(newHandler) : handlers.push(newHandler);\n  } else if (handlers) {\n    events[name] = important ? [newHandler, handlers] : [handlers, newHandler];\n  } else {\n    events[name] = newHandler;\n  }\n}\n\nfunction getBindingAttr (\n  el,\n  name,\n  getStatic\n) {\n  var dynamicValue =\n    getAndRemoveAttr(el, ':' + name) ||\n    getAndRemoveAttr(el, 'v-bind:' + name);\n  if (dynamicValue != null) {\n    return parseFilters(dynamicValue)\n  } else if (getStatic !== false) {\n    var staticValue = getAndRemoveAttr(el, name);\n    if (staticValue != null) {\n      return JSON.stringify(staticValue)\n    }\n  }\n}\n\n// note: this only removes the attr from the Array (attrsList) so that it\n// doesn't get processed by processAttrs.\n// By default it does NOT remove it from the map (attrsMap) because the map is\n// needed during codegen.\nfunction getAndRemoveAttr (\n  el,\n  name,\n  removeFromMap\n) {\n  var val;\n  if ((val = el.attrsMap[name]) != null) {\n    var list = el.attrsList;\n    for (var i = 0, l = list.length; i < l; i++) {\n      if (list[i].name === name) {\n        list.splice(i, 1);\n        break\n      }\n    }\n  }\n  if (removeFromMap) {\n    delete el.attrsMap[name];\n  }\n  return val\n}\n\n/*  */\n\n/**\n * Cross-platform code generation for component v-model\n */\nfunction genComponentModel (\n  el,\n  value,\n  modifiers\n) {\n  var ref = modifiers || {};\n  var number = ref.number;\n  var trim = ref.trim;\n\n  var baseValueExpression = '$$v';\n  var valueExpression = baseValueExpression;\n  if (trim) {\n    valueExpression =\n      \"(typeof \" + baseValueExpression + \" === 'string'\" +\n        \"? \" + baseValueExpression + \".trim()\" +\n        \": \" + baseValueExpression + \")\";\n  }\n  if (number) {\n    valueExpression = \"_n(\" + valueExpression + \")\";\n  }\n  var assignment = genAssignmentCode(value, valueExpression);\n\n  el.model = {\n    value: (\"(\" + value + \")\"),\n    expression: (\"\\\"\" + value + \"\\\"\"),\n    callback: (\"function (\" + baseValueExpression + \") {\" + assignment + \"}\")\n  };\n}\n\n/**\n * Cross-platform codegen helper for generating v-model value assignment code.\n */\nfunction genAssignmentCode (\n  value,\n  assignment\n) {\n  var res = parseModel(value);\n  if (res.key === null) {\n    return (value + \"=\" + assignment)\n  } else {\n    return (\"$set(\" + (res.exp) + \", \" + (res.key) + \", \" + assignment + \")\")\n  }\n}\n\n/**\n * Parse a v-model expression into a base path and a final key segment.\n * Handles both dot-path and possible square brackets.\n *\n * Possible cases:\n *\n * - test\n * - test[key]\n * - test[test1[key]]\n * - test[\"a\"][key]\n * - xxx.test[a[a].test1[key]]\n * - test.xxx.a[\"asa\"][test1[key]]\n *\n */\n\nvar len;\nvar str;\nvar chr;\nvar index$1;\nvar expressionPos;\nvar expressionEndPos;\n\n\n\nfunction parseModel (val) {\n  len = val.length;\n\n  if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) {\n    index$1 = val.lastIndexOf('.');\n    if (index$1 > -1) {\n      return {\n        exp: val.slice(0, index$1),\n        key: '\"' + val.slice(index$1 + 1) + '\"'\n      }\n    } else {\n      return {\n        exp: val,\n        key: null\n      }\n    }\n  }\n\n  str = val;\n  index$1 = expressionPos = expressionEndPos = 0;\n\n  while (!eof()) {\n    chr = next();\n    /* istanbul ignore if */\n    if (isStringStart(chr)) {\n      parseString(chr);\n    } else if (chr === 0x5B) {\n      parseBracket(chr);\n    }\n  }\n\n  return {\n    exp: val.slice(0, expressionPos),\n    key: val.slice(expressionPos + 1, expressionEndPos)\n  }\n}\n\nfunction next () {\n  return str.charCodeAt(++index$1)\n}\n\nfunction eof () {\n  return index$1 >= len\n}\n\nfunction isStringStart (chr) {\n  return chr === 0x22 || chr === 0x27\n}\n\nfunction parseBracket (chr) {\n  var inBracket = 1;\n  expressionPos = index$1;\n  while (!eof()) {\n    chr = next();\n    if (isStringStart(chr)) {\n      parseString(chr);\n      continue\n    }\n    if (chr === 0x5B) { inBracket++; }\n    if (chr === 0x5D) { inBracket--; }\n    if (inBracket === 0) {\n      expressionEndPos = index$1;\n      break\n    }\n  }\n}\n\nfunction parseString (chr) {\n  var stringQuote = chr;\n  while (!eof()) {\n    chr = next();\n    if (chr === stringQuote) {\n      break\n    }\n  }\n}\n\n/*  */\n\nvar warn$1;\n\n// in some cases, the event used has to be determined at runtime\n// so we used some reserved tokens during compile.\nvar RANGE_TOKEN = '__r';\nvar CHECKBOX_RADIO_TOKEN = '__c';\n\nfunction model (\n  el,\n  dir,\n  _warn\n) {\n  warn$1 = _warn;\n  var value = dir.value;\n  var modifiers = dir.modifiers;\n  var tag = el.tag;\n  var type = el.attrsMap.type;\n\n  {\n    // inputs with type=\"file\" are read only and setting the input's\n    // value will throw an error.\n    if (tag === 'input' && type === 'file') {\n      warn$1(\n        \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\" type=\\\"file\\\">:\\n\" +\n        \"File inputs are read only. Use a v-on:change listener instead.\"\n      );\n    }\n  }\n\n  if (el.component) {\n    genComponentModel(el, value, modifiers);\n    // component v-model doesn't need extra runtime\n    return false\n  } else if (tag === 'select') {\n    genSelect(el, value, modifiers);\n  } else if (tag === 'input' && type === 'checkbox') {\n    genCheckboxModel(el, value, modifiers);\n  } else if (tag === 'input' && type === 'radio') {\n    genRadioModel(el, value, modifiers);\n  } else if (tag === 'input' || tag === 'textarea') {\n    genDefaultModel(el, value, modifiers);\n  } else if (!config.isReservedTag(tag)) {\n    genComponentModel(el, value, modifiers);\n    // component v-model doesn't need extra runtime\n    return false\n  } else {\n    warn$1(\n      \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\">: \" +\n      \"v-model is not supported on this element type. \" +\n      'If you are working with contenteditable, it\\'s recommended to ' +\n      'wrap a library dedicated for that purpose inside a custom component.'\n    );\n  }\n\n  // ensure runtime directive metadata\n  return true\n}\n\nfunction genCheckboxModel (\n  el,\n  value,\n  modifiers\n) {\n  var number = modifiers && modifiers.number;\n  var valueBinding = getBindingAttr(el, 'value') || 'null';\n  var trueValueBinding = getBindingAttr(el, 'true-value') || 'true';\n  var falseValueBinding = getBindingAttr(el, 'false-value') || 'false';\n  addProp(el, 'checked',\n    \"Array.isArray(\" + value + \")\" +\n      \"?_i(\" + value + \",\" + valueBinding + \")>-1\" + (\n        trueValueBinding === 'true'\n          ? (\":(\" + value + \")\")\n          : (\":_q(\" + value + \",\" + trueValueBinding + \")\")\n      )\n  );\n  addHandler(el, 'change',\n    \"var $$a=\" + value + \",\" +\n        '$$el=$event.target,' +\n        \"$$c=$$el.checked?(\" + trueValueBinding + \"):(\" + falseValueBinding + \");\" +\n    'if(Array.isArray($$a)){' +\n      \"var $$v=\" + (number ? '_n(' + valueBinding + ')' : valueBinding) + \",\" +\n          '$$i=_i($$a,$$v);' +\n      \"if($$el.checked){$$i<0&&(\" + value + \"=$$a.concat([$$v]))}\" +\n      \"else{$$i>-1&&(\" + value + \"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}\" +\n    \"}else{\" + (genAssignmentCode(value, '$$c')) + \"}\",\n    null, true\n  );\n}\n\nfunction genRadioModel (\n    el,\n    value,\n    modifiers\n) {\n  var number = modifiers && modifiers.number;\n  var valueBinding = getBindingAttr(el, 'value') || 'null';\n  valueBinding = number ? (\"_n(\" + valueBinding + \")\") : valueBinding;\n  addProp(el, 'checked', (\"_q(\" + value + \",\" + valueBinding + \")\"));\n  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true);\n}\n\nfunction genSelect (\n    el,\n    value,\n    modifiers\n) {\n  var number = modifiers && modifiers.number;\n  var selectedVal = \"Array.prototype.filter\" +\n    \".call($event.target.options,function(o){return o.selected})\" +\n    \".map(function(o){var val = \\\"_value\\\" in o ? o._value : o.value;\" +\n    \"return \" + (number ? '_n(val)' : 'val') + \"})\";\n\n  var assignment = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]';\n  var code = \"var $$selectedVal = \" + selectedVal + \";\";\n  code = code + \" \" + (genAssignmentCode(value, assignment));\n  addHandler(el, 'change', code, null, true);\n}\n\nfunction genDefaultModel (\n  el,\n  value,\n  modifiers\n) {\n  var type = el.attrsMap.type;\n\n  // warn if v-bind:value conflicts with v-model\n  {\n    var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];\n    if (value$1) {\n      var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';\n      warn$1(\n        binding + \"=\\\"\" + value$1 + \"\\\" conflicts with v-model on the same element \" +\n        'because the latter already expands to a value binding internally'\n      );\n    }\n  }\n\n  var ref = modifiers || {};\n  var lazy = ref.lazy;\n  var number = ref.number;\n  var trim = ref.trim;\n  var needCompositionGuard = !lazy && type !== 'range';\n  var event = lazy\n    ? 'change'\n    : type === 'range'\n      ? RANGE_TOKEN\n      : 'input';\n\n  var valueExpression = '$event.target.value';\n  if (trim) {\n    valueExpression = \"$event.target.value.trim()\";\n  }\n  if (number) {\n    valueExpression = \"_n(\" + valueExpression + \")\";\n  }\n\n  var code = genAssignmentCode(value, valueExpression);\n  if (needCompositionGuard) {\n    code = \"if($event.target.composing)return;\" + code;\n  }\n\n  addProp(el, 'value', (\"(\" + value + \")\"));\n  addHandler(el, event, code, null, true);\n  if (trim || number) {\n    addHandler(el, 'blur', '$forceUpdate()');\n  }\n}\n\n/*  */\n\n// normalize v-model event tokens that can only be determined at runtime.\n// it's important to place the event as the first in the array because\n// the whole point is ensuring the v-model callback gets called before\n// user-attached handlers.\nfunction normalizeEvents (on) {\n  /* istanbul ignore if */\n  if (isDef(on[RANGE_TOKEN])) {\n    // IE input[type=range] only supports `change` event\n    var event = isIE ? 'change' : 'input';\n    on[event] = [].concat(on[RANGE_TOKEN], on[event] || []);\n    delete on[RANGE_TOKEN];\n  }\n  // This was originally intended to fix #4521 but no longer necessary\n  // after 2.5. Keeping it for backwards compat with generated code from < 2.4\n  /* istanbul ignore if */\n  if (isDef(on[CHECKBOX_RADIO_TOKEN])) {\n    on.change = [].concat(on[CHECKBOX_RADIO_TOKEN], on.change || []);\n    delete on[CHECKBOX_RADIO_TOKEN];\n  }\n}\n\nvar target$1;\n\nfunction createOnceHandler (handler, event, capture) {\n  var _target = target$1; // save current target element in closure\n  return function onceHandler () {\n    var res = handler.apply(null, arguments);\n    if (res !== null) {\n      remove$2(event, onceHandler, capture, _target);\n    }\n  }\n}\n\nfunction add$1 (\n  event,\n  handler,\n  once$$1,\n  capture,\n  passive\n) {\n  handler = withMacroTask(handler);\n  if (once$$1) { handler = createOnceHandler(handler, event, capture); }\n  target$1.addEventListener(\n    event,\n    handler,\n    supportsPassive\n      ? { capture: capture, passive: passive }\n      : capture\n  );\n}\n\nfunction remove$2 (\n  event,\n  handler,\n  capture,\n  _target\n) {\n  (_target || target$1).removeEventListener(\n    event,\n    handler._withTask || handler,\n    capture\n  );\n}\n\nfunction updateDOMListeners (oldVnode, vnode) {\n  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {\n    return\n  }\n  var on = vnode.data.on || {};\n  var oldOn = oldVnode.data.on || {};\n  target$1 = vnode.elm;\n  normalizeEvents(on);\n  updateListeners(on, oldOn, add$1, remove$2, vnode.context);\n  target$1 = undefined;\n}\n\nvar events = {\n  create: updateDOMListeners,\n  update: updateDOMListeners\n};\n\n/*  */\n\nfunction updateDOMProps (oldVnode, vnode) {\n  if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {\n    return\n  }\n  var key, cur;\n  var elm = vnode.elm;\n  var oldProps = oldVnode.data.domProps || {};\n  var props = vnode.data.domProps || {};\n  // clone observed objects, as the user probably wants to mutate it\n  if (isDef(props.__ob__)) {\n    props = vnode.data.domProps = extend({}, props);\n  }\n\n  for (key in oldProps) {\n    if (isUndef(props[key])) {\n      elm[key] = '';\n    }\n  }\n  for (key in props) {\n    cur = props[key];\n    // ignore children if the node has textContent or innerHTML,\n    // as these will throw away existing DOM nodes and cause removal errors\n    // on subsequent patches (#3360)\n    if (key === 'textContent' || key === 'innerHTML') {\n      if (vnode.children) { vnode.children.length = 0; }\n      if (cur === oldProps[key]) { continue }\n      // #6601 work around Chrome version <= 55 bug where single textNode\n      // replaced by innerHTML/textContent retains its parentNode property\n      if (elm.childNodes.length === 1) {\n        elm.removeChild(elm.childNodes[0]);\n      }\n    }\n\n    if (key === 'value') {\n      // store value as _value as well since\n      // non-string values will be stringified\n      elm._value = cur;\n      // avoid resetting cursor position when value is the same\n      var strCur = isUndef(cur) ? '' : String(cur);\n      if (shouldUpdateValue(elm, strCur)) {\n        elm.value = strCur;\n      }\n    } else {\n      elm[key] = cur;\n    }\n  }\n}\n\n// check platforms/web/util/attrs.js acceptValue\n\n\nfunction shouldUpdateValue (elm, checkVal) {\n  return (!elm.composing && (\n    elm.tagName === 'OPTION' ||\n    isNotInFocusAndDirty(elm, checkVal) ||\n    isDirtyWithModifiers(elm, checkVal)\n  ))\n}\n\nfunction isNotInFocusAndDirty (elm, checkVal) {\n  // return true when textbox (.number and .trim) loses focus and its value is\n  // not equal to the updated value\n  var notInFocus = true;\n  // #6157\n  // work around IE bug when accessing document.activeElement in an iframe\n  try { notInFocus = document.activeElement !== elm; } catch (e) {}\n  return notInFocus && elm.value !== checkVal\n}\n\nfunction isDirtyWithModifiers (elm, newVal) {\n  var value = elm.value;\n  var modifiers = elm._vModifiers; // injected by v-model runtime\n  if (isDef(modifiers)) {\n    if (modifiers.lazy) {\n      // inputs with lazy should only be updated when not in focus\n      return false\n    }\n    if (modifiers.number) {\n      return toNumber(value) !== toNumber(newVal)\n    }\n    if (modifiers.trim) {\n      return value.trim() !== newVal.trim()\n    }\n  }\n  return value !== newVal\n}\n\nvar domProps = {\n  create: updateDOMProps,\n  update: updateDOMProps\n};\n\n/*  */\n\nvar parseStyleText = cached(function (cssText) {\n  var res = {};\n  var listDelimiter = /;(?![^(]*\\))/g;\n  var propertyDelimiter = /:(.+)/;\n  cssText.split(listDelimiter).forEach(function (item) {\n    if (item) {\n      var tmp = item.split(propertyDelimiter);\n      tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim());\n    }\n  });\n  return res\n});\n\n// merge static and dynamic style data on the same vnode\nfunction normalizeStyleData (data) {\n  var style = normalizeStyleBinding(data.style);\n  // static style is pre-processed into an object during compilation\n  // and is always a fresh object, so it's safe to merge into it\n  return data.staticStyle\n    ? extend(data.staticStyle, style)\n    : style\n}\n\n// normalize possible array / string values into Object\nfunction normalizeStyleBinding (bindingStyle) {\n  if (Array.isArray(bindingStyle)) {\n    return toObject(bindingStyle)\n  }\n  if (typeof bindingStyle === 'string') {\n    return parseStyleText(bindingStyle)\n  }\n  return bindingStyle\n}\n\n/**\n * parent component style should be after child's\n * so that parent component's style could override it\n */\nfunction getStyle (vnode, checkChild) {\n  var res = {};\n  var styleData;\n\n  if (checkChild) {\n    var childNode = vnode;\n    while (childNode.componentInstance) {\n      childNode = childNode.componentInstance._vnode;\n      if (childNode.data && (styleData = normalizeStyleData(childNode.data))) {\n        extend(res, styleData);\n      }\n    }\n  }\n\n  if ((styleData = normalizeStyleData(vnode.data))) {\n    extend(res, styleData);\n  }\n\n  var parentNode = vnode;\n  while ((parentNode = parentNode.parent)) {\n    if (parentNode.data && (styleData = normalizeStyleData(parentNode.data))) {\n      extend(res, styleData);\n    }\n  }\n  return res\n}\n\n/*  */\n\nvar cssVarRE = /^--/;\nvar importantRE = /\\s*!important$/;\nvar setProp = function (el, name, val) {\n  /* istanbul ignore if */\n  if (cssVarRE.test(name)) {\n    el.style.setProperty(name, val);\n  } else if (importantRE.test(val)) {\n    el.style.setProperty(name, val.replace(importantRE, ''), 'important');\n  } else {\n    var normalizedName = normalize(name);\n    if (Array.isArray(val)) {\n      // Support values array created by autoprefixer, e.g.\n      // {display: [\"-webkit-box\", \"-ms-flexbox\", \"flex\"]}\n      // Set them one by one, and the browser will only set those it can recognize\n      for (var i = 0, len = val.length; i < len; i++) {\n        el.style[normalizedName] = val[i];\n      }\n    } else {\n      el.style[normalizedName] = val;\n    }\n  }\n};\n\nvar vendorNames = ['Webkit', 'Moz', 'ms'];\n\nvar emptyStyle;\nvar normalize = cached(function (prop) {\n  emptyStyle = emptyStyle || document.createElement('div').style;\n  prop = camelize(prop);\n  if (prop !== 'filter' && (prop in emptyStyle)) {\n    return prop\n  }\n  var capName = prop.charAt(0).toUpperCase() + prop.slice(1);\n  for (var i = 0; i < vendorNames.length; i++) {\n    var name = vendorNames[i] + capName;\n    if (name in emptyStyle) {\n      return name\n    }\n  }\n});\n\nfunction updateStyle (oldVnode, vnode) {\n  var data = vnode.data;\n  var oldData = oldVnode.data;\n\n  if (isUndef(data.staticStyle) && isUndef(data.style) &&\n    isUndef(oldData.staticStyle) && isUndef(oldData.style)\n  ) {\n    return\n  }\n\n  var cur, name;\n  var el = vnode.elm;\n  var oldStaticStyle = oldData.staticStyle;\n  var oldStyleBinding = oldData.normalizedStyle || oldData.style || {};\n\n  // if static style exists, stylebinding already merged into it when doing normalizeStyleData\n  var oldStyle = oldStaticStyle || oldStyleBinding;\n\n  var style = normalizeStyleBinding(vnode.data.style) || {};\n\n  // store normalized style under a different key for next diff\n  // make sure to clone it if it's reactive, since the user likely wants\n  // to mutate it.\n  vnode.data.normalizedStyle = isDef(style.__ob__)\n    ? extend({}, style)\n    : style;\n\n  var newStyle = getStyle(vnode, true);\n\n  for (name in oldStyle) {\n    if (isUndef(newStyle[name])) {\n      setProp(el, name, '');\n    }\n  }\n  for (name in newStyle) {\n    cur = newStyle[name];\n    if (cur !== oldStyle[name]) {\n      // ie9 setting to null has no effect, must use empty string\n      setProp(el, name, cur == null ? '' : cur);\n    }\n  }\n}\n\nvar style = {\n  create: updateStyle,\n  update: updateStyle\n};\n\n/*  */\n\n/**\n * Add class with compatibility for SVG since classList is not supported on\n * SVG elements in IE\n */\nfunction addClass (el, cls) {\n  /* istanbul ignore if */\n  if (!cls || !(cls = cls.trim())) {\n    return\n  }\n\n  /* istanbul ignore else */\n  if (el.classList) {\n    if (cls.indexOf(' ') > -1) {\n      cls.split(/\\s+/).forEach(function (c) { return el.classList.add(c); });\n    } else {\n      el.classList.add(cls);\n    }\n  } else {\n    var cur = \" \" + (el.getAttribute('class') || '') + \" \";\n    if (cur.indexOf(' ' + cls + ' ') < 0) {\n      el.setAttribute('class', (cur + cls).trim());\n    }\n  }\n}\n\n/**\n * Remove class with compatibility for SVG since classList is not supported on\n * SVG elements in IE\n */\nfunction removeClass (el, cls) {\n  /* istanbul ignore if */\n  if (!cls || !(cls = cls.trim())) {\n    return\n  }\n\n  /* istanbul ignore else */\n  if (el.classList) {\n    if (cls.indexOf(' ') > -1) {\n      cls.split(/\\s+/).forEach(function (c) { return el.classList.remove(c); });\n    } else {\n      el.classList.remove(cls);\n    }\n    if (!el.classList.length) {\n      el.removeAttribute('class');\n    }\n  } else {\n    var cur = \" \" + (el.getAttribute('class') || '') + \" \";\n    var tar = ' ' + cls + ' ';\n    while (cur.indexOf(tar) >= 0) {\n      cur = cur.replace(tar, ' ');\n    }\n    cur = cur.trim();\n    if (cur) {\n      el.setAttribute('class', cur);\n    } else {\n      el.removeAttribute('class');\n    }\n  }\n}\n\n/*  */\n\nfunction resolveTransition (def) {\n  if (!def) {\n    return\n  }\n  /* istanbul ignore else */\n  if (typeof def === 'object') {\n    var res = {};\n    if (def.css !== false) {\n      extend(res, autoCssTransition(def.name || 'v'));\n    }\n    extend(res, def);\n    return res\n  } else if (typeof def === 'string') {\n    return autoCssTransition(def)\n  }\n}\n\nvar autoCssTransition = cached(function (name) {\n  return {\n    enterClass: (name + \"-enter\"),\n    enterToClass: (name + \"-enter-to\"),\n    enterActiveClass: (name + \"-enter-active\"),\n    leaveClass: (name + \"-leave\"),\n    leaveToClass: (name + \"-leave-to\"),\n    leaveActiveClass: (name + \"-leave-active\")\n  }\n});\n\nvar hasTransition = inBrowser && !isIE9;\nvar TRANSITION = 'transition';\nvar ANIMATION = 'animation';\n\n// Transition property/event sniffing\nvar transitionProp = 'transition';\nvar transitionEndEvent = 'transitionend';\nvar animationProp = 'animation';\nvar animationEndEvent = 'animationend';\nif (hasTransition) {\n  /* istanbul ignore if */\n  if (window.ontransitionend === undefined &&\n    window.onwebkittransitionend !== undefined\n  ) {\n    transitionProp = 'WebkitTransition';\n    transitionEndEvent = 'webkitTransitionEnd';\n  }\n  if (window.onanimationend === undefined &&\n    window.onwebkitanimationend !== undefined\n  ) {\n    animationProp = 'WebkitAnimation';\n    animationEndEvent = 'webkitAnimationEnd';\n  }\n}\n\n// binding to window is necessary to make hot reload work in IE in strict mode\nvar raf = inBrowser\n  ? window.requestAnimationFrame\n    ? window.requestAnimationFrame.bind(window)\n    : setTimeout\n  : /* istanbul ignore next */ function (fn) { return fn(); };\n\nfunction nextFrame (fn) {\n  raf(function () {\n    raf(fn);\n  });\n}\n\nfunction addTransitionClass (el, cls) {\n  var transitionClasses = el._transitionClasses || (el._transitionClasses = []);\n  if (transitionClasses.indexOf(cls) < 0) {\n    transitionClasses.push(cls);\n    addClass(el, cls);\n  }\n}\n\nfunction removeTransitionClass (el, cls) {\n  if (el._transitionClasses) {\n    remove(el._transitionClasses, cls);\n  }\n  removeClass(el, cls);\n}\n\nfunction whenTransitionEnds (\n  el,\n  expectedType,\n  cb\n) {\n  var ref = getTransitionInfo(el, expectedType);\n  var type = ref.type;\n  var timeout = ref.timeout;\n  var propCount = ref.propCount;\n  if (!type) { return cb() }\n  var event = type === TRANSITION ? transitionEndEvent : animationEndEvent;\n  var ended = 0;\n  var end = function () {\n    el.removeEventListener(event, onEnd);\n    cb();\n  };\n  var onEnd = function (e) {\n    if (e.target === el) {\n      if (++ended >= propCount) {\n        end();\n      }\n    }\n  };\n  setTimeout(function () {\n    if (ended < propCount) {\n      end();\n    }\n  }, timeout + 1);\n  el.addEventListener(event, onEnd);\n}\n\nvar transformRE = /\\b(transform|all)(,|$)/;\n\nfunction getTransitionInfo (el, expectedType) {\n  var styles = window.getComputedStyle(el);\n  var transitionDelays = styles[transitionProp + 'Delay'].split(', ');\n  var transitionDurations = styles[transitionProp + 'Duration'].split(', ');\n  var transitionTimeout = getTimeout(transitionDelays, transitionDurations);\n  var animationDelays = styles[animationProp + 'Delay'].split(', ');\n  var animationDurations = styles[animationProp + 'Duration'].split(', ');\n  var animationTimeout = getTimeout(animationDelays, animationDurations);\n\n  var type;\n  var timeout = 0;\n  var propCount = 0;\n  /* istanbul ignore if */\n  if (expectedType === TRANSITION) {\n    if (transitionTimeout > 0) {\n      type = TRANSITION;\n      timeout = transitionTimeout;\n      propCount = transitionDurations.length;\n    }\n  } else if (expectedType === ANIMATION) {\n    if (animationTimeout > 0) {\n      type = ANIMATION;\n      timeout = animationTimeout;\n      propCount = animationDurations.length;\n    }\n  } else {\n    timeout = Math.max(transitionTimeout, animationTimeout);\n    type = timeout > 0\n      ? transitionTimeout > animationTimeout\n        ? TRANSITION\n        : ANIMATION\n      : null;\n    propCount = type\n      ? type === TRANSITION\n        ? transitionDurations.length\n        : animationDurations.length\n      : 0;\n  }\n  var hasTransform =\n    type === TRANSITION &&\n    transformRE.test(styles[transitionProp + 'Property']);\n  return {\n    type: type,\n    timeout: timeout,\n    propCount: propCount,\n    hasTransform: hasTransform\n  }\n}\n\nfunction getTimeout (delays, durations) {\n  /* istanbul ignore next */\n  while (delays.length < durations.length) {\n    delays = delays.concat(delays);\n  }\n\n  return Math.max.apply(null, durations.map(function (d, i) {\n    return toMs(d) + toMs(delays[i])\n  }))\n}\n\nfunction toMs (s) {\n  return Number(s.slice(0, -1)) * 1000\n}\n\n/*  */\n\nfunction enter (vnode, toggleDisplay) {\n  var el = vnode.elm;\n\n  // call leave callback now\n  if (isDef(el._leaveCb)) {\n    el._leaveCb.cancelled = true;\n    el._leaveCb();\n  }\n\n  var data = resolveTransition(vnode.data.transition);\n  if (isUndef(data)) {\n    return\n  }\n\n  /* istanbul ignore if */\n  if (isDef(el._enterCb) || el.nodeType !== 1) {\n    return\n  }\n\n  var css = data.css;\n  var type = data.type;\n  var enterClass = data.enterClass;\n  var enterToClass = data.enterToClass;\n  var enterActiveClass = data.enterActiveClass;\n  var appearClass = data.appearClass;\n  var appearToClass = data.appearToClass;\n  var appearActiveClass = data.appearActiveClass;\n  var beforeEnter = data.beforeEnter;\n  var enter = data.enter;\n  var afterEnter = data.afterEnter;\n  var enterCancelled = data.enterCancelled;\n  var beforeAppear = data.beforeAppear;\n  var appear = data.appear;\n  var afterAppear = data.afterAppear;\n  var appearCancelled = data.appearCancelled;\n  var duration = data.duration;\n\n  // activeInstance will always be the <transition> component managing this\n  // transition. One edge case to check is when the <transition> is placed\n  // as the root node of a child component. In that case we need to check\n  // <transition>'s parent for appear check.\n  var context = activeInstance;\n  var transitionNode = activeInstance.$vnode;\n  while (transitionNode && transitionNode.parent) {\n    transitionNode = transitionNode.parent;\n    context = transitionNode.context;\n  }\n\n  var isAppear = !context._isMounted || !vnode.isRootInsert;\n\n  if (isAppear && !appear && appear !== '') {\n    return\n  }\n\n  var startClass = isAppear && appearClass\n    ? appearClass\n    : enterClass;\n  var activeClass = isAppear && appearActiveClass\n    ? appearActiveClass\n    : enterActiveClass;\n  var toClass = isAppear && appearToClass\n    ? appearToClass\n    : enterToClass;\n\n  var beforeEnterHook = isAppear\n    ? (beforeAppear || beforeEnter)\n    : beforeEnter;\n  var enterHook = isAppear\n    ? (typeof appear === 'function' ? appear : enter)\n    : enter;\n  var afterEnterHook = isAppear\n    ? (afterAppear || afterEnter)\n    : afterEnter;\n  var enterCancelledHook = isAppear\n    ? (appearCancelled || enterCancelled)\n    : enterCancelled;\n\n  var explicitEnterDuration = toNumber(\n    isObject(duration)\n      ? duration.enter\n      : duration\n  );\n\n  if (\"development\" !== 'production' && explicitEnterDuration != null) {\n    checkDuration(explicitEnterDuration, 'enter', vnode);\n  }\n\n  var expectsCSS = css !== false && !isIE9;\n  var userWantsControl = getHookArgumentsLength(enterHook);\n\n  var cb = el._enterCb = once(function () {\n    if (expectsCSS) {\n      removeTransitionClass(el, toClass);\n      removeTransitionClass(el, activeClass);\n    }\n    if (cb.cancelled) {\n      if (expectsCSS) {\n        removeTransitionClass(el, startClass);\n      }\n      enterCancelledHook && enterCancelledHook(el);\n    } else {\n      afterEnterHook && afterEnterHook(el);\n    }\n    el._enterCb = null;\n  });\n\n  if (!vnode.data.show) {\n    // remove pending leave element on enter by injecting an insert hook\n    mergeVNodeHook(vnode, 'insert', function () {\n      var parent = el.parentNode;\n      var pendingNode = parent && parent._pending && parent._pending[vnode.key];\n      if (pendingNode &&\n        pendingNode.tag === vnode.tag &&\n        pendingNode.elm._leaveCb\n      ) {\n        pendingNode.elm._leaveCb();\n      }\n      enterHook && enterHook(el, cb);\n    });\n  }\n\n  // start enter transition\n  beforeEnterHook && beforeEnterHook(el);\n  if (expectsCSS) {\n    addTransitionClass(el, startClass);\n    addTransitionClass(el, activeClass);\n    nextFrame(function () {\n      addTransitionClass(el, toClass);\n      removeTransitionClass(el, startClass);\n      if (!cb.cancelled && !userWantsControl) {\n        if (isValidDuration(explicitEnterDuration)) {\n          setTimeout(cb, explicitEnterDuration);\n        } else {\n          whenTransitionEnds(el, type, cb);\n        }\n      }\n    });\n  }\n\n  if (vnode.data.show) {\n    toggleDisplay && toggleDisplay();\n    enterHook && enterHook(el, cb);\n  }\n\n  if (!expectsCSS && !userWantsControl) {\n    cb();\n  }\n}\n\nfunction leave (vnode, rm) {\n  var el = vnode.elm;\n\n  // call enter callback now\n  if (isDef(el._enterCb)) {\n    el._enterCb.cancelled = true;\n    el._enterCb();\n  }\n\n  var data = resolveTransition(vnode.data.transition);\n  if (isUndef(data) || el.nodeType !== 1) {\n    return rm()\n  }\n\n  /* istanbul ignore if */\n  if (isDef(el._leaveCb)) {\n    return\n  }\n\n  var css = data.css;\n  var type = data.type;\n  var leaveClass = data.leaveClass;\n  var leaveToClass = data.leaveToClass;\n  var leaveActiveClass = data.leaveActiveClass;\n  var beforeLeave = data.beforeLeave;\n  var leave = data.leave;\n  var afterLeave = data.afterLeave;\n  var leaveCancelled = data.leaveCancelled;\n  var delayLeave = data.delayLeave;\n  var duration = data.duration;\n\n  var expectsCSS = css !== false && !isIE9;\n  var userWantsControl = getHookArgumentsLength(leave);\n\n  var explicitLeaveDuration = toNumber(\n    isObject(duration)\n      ? duration.leave\n      : duration\n  );\n\n  if (\"development\" !== 'production' && isDef(explicitLeaveDuration)) {\n    checkDuration(explicitLeaveDuration, 'leave', vnode);\n  }\n\n  var cb = el._leaveCb = once(function () {\n    if (el.parentNode && el.parentNode._pending) {\n      el.parentNode._pending[vnode.key] = null;\n    }\n    if (expectsCSS) {\n      removeTransitionClass(el, leaveToClass);\n      removeTransitionClass(el, leaveActiveClass);\n    }\n    if (cb.cancelled) {\n      if (expectsCSS) {\n        removeTransitionClass(el, leaveClass);\n      }\n      leaveCancelled && leaveCancelled(el);\n    } else {\n      rm();\n      afterLeave && afterLeave(el);\n    }\n    el._leaveCb = null;\n  });\n\n  if (delayLeave) {\n    delayLeave(performLeave);\n  } else {\n    performLeave();\n  }\n\n  function performLeave () {\n    // the delayed leave may have already been cancelled\n    if (cb.cancelled) {\n      return\n    }\n    // record leaving element\n    if (!vnode.data.show) {\n      (el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key)] = vnode;\n    }\n    beforeLeave && beforeLeave(el);\n    if (expectsCSS) {\n      addTransitionClass(el, leaveClass);\n      addTransitionClass(el, leaveActiveClass);\n      nextFrame(function () {\n        addTransitionClass(el, leaveToClass);\n        removeTransitionClass(el, leaveClass);\n        if (!cb.cancelled && !userWantsControl) {\n          if (isValidDuration(explicitLeaveDuration)) {\n            setTimeout(cb, explicitLeaveDuration);\n          } else {\n            whenTransitionEnds(el, type, cb);\n          }\n        }\n      });\n    }\n    leave && leave(el, cb);\n    if (!expectsCSS && !userWantsControl) {\n      cb();\n    }\n  }\n}\n\n// only used in dev mode\nfunction checkDuration (val, name, vnode) {\n  if (typeof val !== 'number') {\n    warn(\n      \"<transition> explicit \" + name + \" duration is not a valid number - \" +\n      \"got \" + (JSON.stringify(val)) + \".\",\n      vnode.context\n    );\n  } else if (isNaN(val)) {\n    warn(\n      \"<transition> explicit \" + name + \" duration is NaN - \" +\n      'the duration expression might be incorrect.',\n      vnode.context\n    );\n  }\n}\n\nfunction isValidDuration (val) {\n  return typeof val === 'number' && !isNaN(val)\n}\n\n/**\n * Normalize a transition hook's argument length. The hook may be:\n * - a merged hook (invoker) with the original in .fns\n * - a wrapped component method (check ._length)\n * - a plain function (.length)\n */\nfunction getHookArgumentsLength (fn) {\n  if (isUndef(fn)) {\n    return false\n  }\n  var invokerFns = fn.fns;\n  if (isDef(invokerFns)) {\n    // invoker\n    return getHookArgumentsLength(\n      Array.isArray(invokerFns)\n        ? invokerFns[0]\n        : invokerFns\n    )\n  } else {\n    return (fn._length || fn.length) > 1\n  }\n}\n\nfunction _enter (_, vnode) {\n  if (vnode.data.show !== true) {\n    enter(vnode);\n  }\n}\n\nvar transition = inBrowser ? {\n  create: _enter,\n  activate: _enter,\n  remove: function remove$$1 (vnode, rm) {\n    /* istanbul ignore else */\n    if (vnode.data.show !== true) {\n      leave(vnode, rm);\n    } else {\n      rm();\n    }\n  }\n} : {};\n\nvar platformModules = [\n  attrs,\n  klass,\n  events,\n  domProps,\n  style,\n  transition\n];\n\n/*  */\n\n// the directive module should be applied last, after all\n// built-in modules have been applied.\nvar modules = platformModules.concat(baseModules);\n\nvar patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });\n\n/**\n * Not type checking this file because flow doesn't like attaching\n * properties to Elements.\n */\n\n/* istanbul ignore if */\nif (isIE9) {\n  // http://www.matts411.com/post/internet-explorer-9-oninput/\n  document.addEventListener('selectionchange', function () {\n    var el = document.activeElement;\n    if (el && el.vmodel) {\n      trigger(el, 'input');\n    }\n  });\n}\n\nvar directive = {\n  inserted: function inserted (el, binding, vnode, oldVnode) {\n    if (vnode.tag === 'select') {\n      // #6903\n      if (oldVnode.elm && !oldVnode.elm._vOptions) {\n        mergeVNodeHook(vnode, 'postpatch', function () {\n          directive.componentUpdated(el, binding, vnode);\n        });\n      } else {\n        setSelected(el, binding, vnode.context);\n      }\n      el._vOptions = [].map.call(el.options, getValue);\n    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {\n      el._vModifiers = binding.modifiers;\n      if (!binding.modifiers.lazy) {\n        // Safari < 10.2 & UIWebView doesn't fire compositionend when\n        // switching focus before confirming composition choice\n        // this also fixes the issue where some browsers e.g. iOS Chrome\n        // fires \"change\" instead of \"input\" on autocomplete.\n        el.addEventListener('change', onCompositionEnd);\n        if (!isAndroid) {\n          el.addEventListener('compositionstart', onCompositionStart);\n          el.addEventListener('compositionend', onCompositionEnd);\n        }\n        /* istanbul ignore if */\n        if (isIE9) {\n          el.vmodel = true;\n        }\n      }\n    }\n  },\n\n  componentUpdated: function componentUpdated (el, binding, vnode) {\n    if (vnode.tag === 'select') {\n      setSelected(el, binding, vnode.context);\n      // in case the options rendered by v-for have changed,\n      // it's possible that the value is out-of-sync with the rendered options.\n      // detect such cases and filter out values that no longer has a matching\n      // option in the DOM.\n      var prevOptions = el._vOptions;\n      var curOptions = el._vOptions = [].map.call(el.options, getValue);\n      if (curOptions.some(function (o, i) { return !looseEqual(o, prevOptions[i]); })) {\n        // trigger change event if\n        // no matching option found for at least one value\n        var needReset = el.multiple\n          ? binding.value.some(function (v) { return hasNoMatchingOption(v, curOptions); })\n          : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions);\n        if (needReset) {\n          trigger(el, 'change');\n        }\n      }\n    }\n  }\n};\n\nfunction setSelected (el, binding, vm) {\n  actuallySetSelected(el, binding, vm);\n  /* istanbul ignore if */\n  if (isIE || isEdge) {\n    setTimeout(function () {\n      actuallySetSelected(el, binding, vm);\n    }, 0);\n  }\n}\n\nfunction actuallySetSelected (el, binding, vm) {\n  var value = binding.value;\n  var isMultiple = el.multiple;\n  if (isMultiple && !Array.isArray(value)) {\n    \"development\" !== 'production' && warn(\n      \"<select multiple v-model=\\\"\" + (binding.expression) + \"\\\"> \" +\n      \"expects an Array value for its binding, but got \" + (Object.prototype.toString.call(value).slice(8, -1)),\n      vm\n    );\n    return\n  }\n  var selected, option;\n  for (var i = 0, l = el.options.length; i < l; i++) {\n    option = el.options[i];\n    if (isMultiple) {\n      selected = looseIndexOf(value, getValue(option)) > -1;\n      if (option.selected !== selected) {\n        option.selected = selected;\n      }\n    } else {\n      if (looseEqual(getValue(option), value)) {\n        if (el.selectedIndex !== i) {\n          el.selectedIndex = i;\n        }\n        return\n      }\n    }\n  }\n  if (!isMultiple) {\n    el.selectedIndex = -1;\n  }\n}\n\nfunction hasNoMatchingOption (value, options) {\n  return options.every(function (o) { return !looseEqual(o, value); })\n}\n\nfunction getValue (option) {\n  return '_value' in option\n    ? option._value\n    : option.value\n}\n\nfunction onCompositionStart (e) {\n  e.target.composing = true;\n}\n\nfunction onCompositionEnd (e) {\n  // prevent triggering an input event for no reason\n  if (!e.target.composing) { return }\n  e.target.composing = false;\n  trigger(e.target, 'input');\n}\n\nfunction trigger (el, type) {\n  var e = document.createEvent('HTMLEvents');\n  e.initEvent(type, true, true);\n  el.dispatchEvent(e);\n}\n\n/*  */\n\n// recursively search for possible transition defined inside the component root\nfunction locateNode (vnode) {\n  return vnode.componentInstance && (!vnode.data || !vnode.data.transition)\n    ? locateNode(vnode.componentInstance._vnode)\n    : vnode\n}\n\nvar show = {\n  bind: function bind (el, ref, vnode) {\n    var value = ref.value;\n\n    vnode = locateNode(vnode);\n    var transition$$1 = vnode.data && vnode.data.transition;\n    var originalDisplay = el.__vOriginalDisplay =\n      el.style.display === 'none' ? '' : el.style.display;\n    if (value && transition$$1) {\n      vnode.data.show = true;\n      enter(vnode, function () {\n        el.style.display = originalDisplay;\n      });\n    } else {\n      el.style.display = value ? originalDisplay : 'none';\n    }\n  },\n\n  update: function update (el, ref, vnode) {\n    var value = ref.value;\n    var oldValue = ref.oldValue;\n\n    /* istanbul ignore if */\n    if (value === oldValue) { return }\n    vnode = locateNode(vnode);\n    var transition$$1 = vnode.data && vnode.data.transition;\n    if (transition$$1) {\n      vnode.data.show = true;\n      if (value) {\n        enter(vnode, function () {\n          el.style.display = el.__vOriginalDisplay;\n        });\n      } else {\n        leave(vnode, function () {\n          el.style.display = 'none';\n        });\n      }\n    } else {\n      el.style.display = value ? el.__vOriginalDisplay : 'none';\n    }\n  },\n\n  unbind: function unbind (\n    el,\n    binding,\n    vnode,\n    oldVnode,\n    isDestroy\n  ) {\n    if (!isDestroy) {\n      el.style.display = el.__vOriginalDisplay;\n    }\n  }\n};\n\nvar platformDirectives = {\n  model: directive,\n  show: show\n};\n\n/*  */\n\n// Provides transition support for a single element/component.\n// supports transition mode (out-in / in-out)\n\nvar transitionProps = {\n  name: String,\n  appear: Boolean,\n  css: Boolean,\n  mode: String,\n  type: String,\n  enterClass: String,\n  leaveClass: String,\n  enterToClass: String,\n  leaveToClass: String,\n  enterActiveClass: String,\n  leaveActiveClass: String,\n  appearClass: String,\n  appearActiveClass: String,\n  appearToClass: String,\n  duration: [Number, String, Object]\n};\n\n// in case the child is also an abstract component, e.g. <keep-alive>\n// we want to recursively retrieve the real component to be rendered\nfunction getRealChild (vnode) {\n  var compOptions = vnode && vnode.componentOptions;\n  if (compOptions && compOptions.Ctor.options.abstract) {\n    return getRealChild(getFirstComponentChild(compOptions.children))\n  } else {\n    return vnode\n  }\n}\n\nfunction extractTransitionData (comp) {\n  var data = {};\n  var options = comp.$options;\n  // props\n  for (var key in options.propsData) {\n    data[key] = comp[key];\n  }\n  // events.\n  // extract listeners and pass them directly to the transition methods\n  var listeners = options._parentListeners;\n  for (var key$1 in listeners) {\n    data[camelize(key$1)] = listeners[key$1];\n  }\n  return data\n}\n\nfunction placeholder (h, rawChild) {\n  if (/\\d-keep-alive$/.test(rawChild.tag)) {\n    return h('keep-alive', {\n      props: rawChild.componentOptions.propsData\n    })\n  }\n}\n\nfunction hasParentTransition (vnode) {\n  while ((vnode = vnode.parent)) {\n    if (vnode.data.transition) {\n      return true\n    }\n  }\n}\n\nfunction isSameChild (child, oldChild) {\n  return oldChild.key === child.key && oldChild.tag === child.tag\n}\n\nvar Transition = {\n  name: 'transition',\n  props: transitionProps,\n  abstract: true,\n\n  render: function render (h) {\n    var this$1 = this;\n\n    var children = this.$slots.default;\n    if (!children) {\n      return\n    }\n\n    // filter out text nodes (possible whitespaces)\n    children = children.filter(function (c) { return c.tag || isAsyncPlaceholder(c); });\n    /* istanbul ignore if */\n    if (!children.length) {\n      return\n    }\n\n    // warn multiple elements\n    if (\"development\" !== 'production' && children.length > 1) {\n      warn(\n        '<transition> can only be used on a single element. Use ' +\n        '<transition-group> for lists.',\n        this.$parent\n      );\n    }\n\n    var mode = this.mode;\n\n    // warn invalid mode\n    if (\"development\" !== 'production' &&\n      mode && mode !== 'in-out' && mode !== 'out-in'\n    ) {\n      warn(\n        'invalid <transition> mode: ' + mode,\n        this.$parent\n      );\n    }\n\n    var rawChild = children[0];\n\n    // if this is a component root node and the component's\n    // parent container node also has transition, skip.\n    if (hasParentTransition(this.$vnode)) {\n      return rawChild\n    }\n\n    // apply transition data to child\n    // use getRealChild() to ignore abstract components e.g. keep-alive\n    var child = getRealChild(rawChild);\n    /* istanbul ignore if */\n    if (!child) {\n      return rawChild\n    }\n\n    if (this._leaving) {\n      return placeholder(h, rawChild)\n    }\n\n    // ensure a key that is unique to the vnode type and to this transition\n    // component instance. This key will be used to remove pending leaving nodes\n    // during entering.\n    var id = \"__transition-\" + (this._uid) + \"-\";\n    child.key = child.key == null\n      ? child.isComment\n        ? id + 'comment'\n        : id + child.tag\n      : isPrimitive(child.key)\n        ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)\n        : child.key;\n\n    var data = (child.data || (child.data = {})).transition = extractTransitionData(this);\n    var oldRawChild = this._vnode;\n    var oldChild = getRealChild(oldRawChild);\n\n    // mark v-show\n    // so that the transition module can hand over the control to the directive\n    if (child.data.directives && child.data.directives.some(function (d) { return d.name === 'show'; })) {\n      child.data.show = true;\n    }\n\n    if (\n      oldChild &&\n      oldChild.data &&\n      !isSameChild(child, oldChild) &&\n      !isAsyncPlaceholder(oldChild) &&\n      // #6687 component root is a comment node\n      !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)\n    ) {\n      // replace old child transition data with fresh one\n      // important for dynamic transitions!\n      var oldData = oldChild.data.transition = extend({}, data);\n      // handle transition mode\n      if (mode === 'out-in') {\n        // return placeholder node and queue update when leave finishes\n        this._leaving = true;\n        mergeVNodeHook(oldData, 'afterLeave', function () {\n          this$1._leaving = false;\n          this$1.$forceUpdate();\n        });\n        return placeholder(h, rawChild)\n      } else if (mode === 'in-out') {\n        if (isAsyncPlaceholder(child)) {\n          return oldRawChild\n        }\n        var delayedLeave;\n        var performLeave = function () { delayedLeave(); };\n        mergeVNodeHook(data, 'afterEnter', performLeave);\n        mergeVNodeHook(data, 'enterCancelled', performLeave);\n        mergeVNodeHook(oldData, 'delayLeave', function (leave) { delayedLeave = leave; });\n      }\n    }\n\n    return rawChild\n  }\n};\n\n/*  */\n\n// Provides transition support for list items.\n// supports move transitions using the FLIP technique.\n\n// Because the vdom's children update algorithm is \"unstable\" - i.e.\n// it doesn't guarantee the relative positioning of removed elements,\n// we force transition-group to update its children into two passes:\n// in the first pass, we remove all nodes that need to be removed,\n// triggering their leaving transition; in the second pass, we insert/move\n// into the final desired state. This way in the second pass removed\n// nodes will remain where they should be.\n\nvar props = extend({\n  tag: String,\n  moveClass: String\n}, transitionProps);\n\ndelete props.mode;\n\nvar TransitionGroup = {\n  props: props,\n\n  render: function render (h) {\n    var tag = this.tag || this.$vnode.data.tag || 'span';\n    var map = Object.create(null);\n    var prevChildren = this.prevChildren = this.children;\n    var rawChildren = this.$slots.default || [];\n    var children = this.children = [];\n    var transitionData = extractTransitionData(this);\n\n    for (var i = 0; i < rawChildren.length; i++) {\n      var c = rawChildren[i];\n      if (c.tag) {\n        if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {\n          children.push(c);\n          map[c.key] = c\n          ;(c.data || (c.data = {})).transition = transitionData;\n        } else {\n          var opts = c.componentOptions;\n          var name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag;\n          warn((\"<transition-group> children must be keyed: <\" + name + \">\"));\n        }\n      }\n    }\n\n    if (prevChildren) {\n      var kept = [];\n      var removed = [];\n      for (var i$1 = 0; i$1 < prevChildren.length; i$1++) {\n        var c$1 = prevChildren[i$1];\n        c$1.data.transition = transitionData;\n        c$1.data.pos = c$1.elm.getBoundingClientRect();\n        if (map[c$1.key]) {\n          kept.push(c$1);\n        } else {\n          removed.push(c$1);\n        }\n      }\n      this.kept = h(tag, null, kept);\n      this.removed = removed;\n    }\n\n    return h(tag, null, children)\n  },\n\n  beforeUpdate: function beforeUpdate () {\n    // force removing pass\n    this.__patch__(\n      this._vnode,\n      this.kept,\n      false, // hydrating\n      true // removeOnly (!important, avoids unnecessary moves)\n    );\n    this._vnode = this.kept;\n  },\n\n  updated: function updated () {\n    var children = this.prevChildren;\n    var moveClass = this.moveClass || ((this.name || 'v') + '-move');\n    if (!children.length || !this.hasMove(children[0].elm, moveClass)) {\n      return\n    }\n\n    // we divide the work into three loops to avoid mixing DOM reads and writes\n    // in each iteration - which helps prevent layout thrashing.\n    children.forEach(callPendingCbs);\n    children.forEach(recordPosition);\n    children.forEach(applyTranslation);\n\n    // force reflow to put everything in position\n    // assign to this to avoid being removed in tree-shaking\n    // $flow-disable-line\n    this._reflow = document.body.offsetHeight;\n\n    children.forEach(function (c) {\n      if (c.data.moved) {\n        var el = c.elm;\n        var s = el.style;\n        addTransitionClass(el, moveClass);\n        s.transform = s.WebkitTransform = s.transitionDuration = '';\n        el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {\n          if (!e || /transform$/.test(e.propertyName)) {\n            el.removeEventListener(transitionEndEvent, cb);\n            el._moveCb = null;\n            removeTransitionClass(el, moveClass);\n          }\n        });\n      }\n    });\n  },\n\n  methods: {\n    hasMove: function hasMove (el, moveClass) {\n      /* istanbul ignore if */\n      if (!hasTransition) {\n        return false\n      }\n      /* istanbul ignore if */\n      if (this._hasMove) {\n        return this._hasMove\n      }\n      // Detect whether an element with the move class applied has\n      // CSS transitions. Since the element may be inside an entering\n      // transition at this very moment, we make a clone of it and remove\n      // all other transition classes applied to ensure only the move class\n      // is applied.\n      var clone = el.cloneNode();\n      if (el._transitionClasses) {\n        el._transitionClasses.forEach(function (cls) { removeClass(clone, cls); });\n      }\n      addClass(clone, moveClass);\n      clone.style.display = 'none';\n      this.$el.appendChild(clone);\n      var info = getTransitionInfo(clone);\n      this.$el.removeChild(clone);\n      return (this._hasMove = info.hasTransform)\n    }\n  }\n};\n\nfunction callPendingCbs (c) {\n  /* istanbul ignore if */\n  if (c.elm._moveCb) {\n    c.elm._moveCb();\n  }\n  /* istanbul ignore if */\n  if (c.elm._enterCb) {\n    c.elm._enterCb();\n  }\n}\n\nfunction recordPosition (c) {\n  c.data.newPos = c.elm.getBoundingClientRect();\n}\n\nfunction applyTranslation (c) {\n  var oldPos = c.data.pos;\n  var newPos = c.data.newPos;\n  var dx = oldPos.left - newPos.left;\n  var dy = oldPos.top - newPos.top;\n  if (dx || dy) {\n    c.data.moved = true;\n    var s = c.elm.style;\n    s.transform = s.WebkitTransform = \"translate(\" + dx + \"px,\" + dy + \"px)\";\n    s.transitionDuration = '0s';\n  }\n}\n\nvar platformComponents = {\n  Transition: Transition,\n  TransitionGroup: TransitionGroup\n};\n\n/*  */\n\n// install platform specific utils\nVue$3.config.mustUseProp = mustUseProp;\nVue$3.config.isReservedTag = isReservedTag;\nVue$3.config.isReservedAttr = isReservedAttr;\nVue$3.config.getTagNamespace = getTagNamespace;\nVue$3.config.isUnknownElement = isUnknownElement;\n\n// install platform runtime directives & components\nextend(Vue$3.options.directives, platformDirectives);\nextend(Vue$3.options.components, platformComponents);\n\n// install platform patch function\nVue$3.prototype.__patch__ = inBrowser ? patch : noop;\n\n// public mount method\nVue$3.prototype.$mount = function (\n  el,\n  hydrating\n) {\n  el = el && inBrowser ? query(el) : undefined;\n  return mountComponent(this, el, hydrating)\n};\n\n// devtools global hook\n/* istanbul ignore next */\nVue$3.nextTick(function () {\n  if (config.devtools) {\n    if (devtools) {\n      devtools.emit('init', Vue$3);\n    } else if (\"development\" !== 'production' && isChrome) {\n      console[console.info ? 'info' : 'log'](\n        'Download the Vue Devtools extension for a better development experience:\\n' +\n        'https://github.com/vuejs/vue-devtools'\n      );\n    }\n  }\n  if (\"development\" !== 'production' &&\n    config.productionTip !== false &&\n    inBrowser && typeof console !== 'undefined'\n  ) {\n    console[console.info ? 'info' : 'log'](\n      \"You are running Vue in development mode.\\n\" +\n      \"Make sure to turn on production mode when deploying for production.\\n\" +\n      \"See more tips at https://vuejs.org/guide/deployment.html\"\n    );\n  }\n}, 0);\n\n/*  */\n\nvar defaultTagRE = /\\{\\{((?:.|\\n)+?)\\}\\}/g;\nvar regexEscapeRE = /[-.*+?^${}()|[\\]\\/\\\\]/g;\n\nvar buildRegex = cached(function (delimiters) {\n  var open = delimiters[0].replace(regexEscapeRE, '\\\\$&');\n  var close = delimiters[1].replace(regexEscapeRE, '\\\\$&');\n  return new RegExp(open + '((?:.|\\\\n)+?)' + close, 'g')\n});\n\nfunction parseText (\n  text,\n  delimiters\n) {\n  var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;\n  if (!tagRE.test(text)) {\n    return\n  }\n  var tokens = [];\n  var lastIndex = tagRE.lastIndex = 0;\n  var match, index;\n  while ((match = tagRE.exec(text))) {\n    index = match.index;\n    // push text token\n    if (index > lastIndex) {\n      tokens.push(JSON.stringify(text.slice(lastIndex, index)));\n    }\n    // tag token\n    var exp = parseFilters(match[1].trim());\n    tokens.push((\"_s(\" + exp + \")\"));\n    lastIndex = index + match[0].length;\n  }\n  if (lastIndex < text.length) {\n    tokens.push(JSON.stringify(text.slice(lastIndex)));\n  }\n  return tokens.join('+')\n}\n\n/*  */\n\nfunction transformNode (el, options) {\n  var warn = options.warn || baseWarn;\n  var staticClass = getAndRemoveAttr(el, 'class');\n  if (\"development\" !== 'production' && staticClass) {\n    var expression = parseText(staticClass, options.delimiters);\n    if (expression) {\n      warn(\n        \"class=\\\"\" + staticClass + \"\\\": \" +\n        'Interpolation inside attributes has been removed. ' +\n        'Use v-bind or the colon shorthand instead. For example, ' +\n        'instead of <div class=\"{{ val }}\">, use <div :class=\"val\">.'\n      );\n    }\n  }\n  if (staticClass) {\n    el.staticClass = JSON.stringify(staticClass);\n  }\n  var classBinding = getBindingAttr(el, 'class', false /* getStatic */);\n  if (classBinding) {\n    el.classBinding = classBinding;\n  }\n}\n\nfunction genData (el) {\n  var data = '';\n  if (el.staticClass) {\n    data += \"staticClass:\" + (el.staticClass) + \",\";\n  }\n  if (el.classBinding) {\n    data += \"class:\" + (el.classBinding) + \",\";\n  }\n  return data\n}\n\nvar klass$1 = {\n  staticKeys: ['staticClass'],\n  transformNode: transformNode,\n  genData: genData\n};\n\n/*  */\n\nfunction transformNode$1 (el, options) {\n  var warn = options.warn || baseWarn;\n  var staticStyle = getAndRemoveAttr(el, 'style');\n  if (staticStyle) {\n    /* istanbul ignore if */\n    {\n      var expression = parseText(staticStyle, options.delimiters);\n      if (expression) {\n        warn(\n          \"style=\\\"\" + staticStyle + \"\\\": \" +\n          'Interpolation inside attributes has been removed. ' +\n          'Use v-bind or the colon shorthand instead. For example, ' +\n          'instead of <div style=\"{{ val }}\">, use <div :style=\"val\">.'\n        );\n      }\n    }\n    el.staticStyle = JSON.stringify(parseStyleText(staticStyle));\n  }\n\n  var styleBinding = getBindingAttr(el, 'style', false /* getStatic */);\n  if (styleBinding) {\n    el.styleBinding = styleBinding;\n  }\n}\n\nfunction genData$1 (el) {\n  var data = '';\n  if (el.staticStyle) {\n    data += \"staticStyle:\" + (el.staticStyle) + \",\";\n  }\n  if (el.styleBinding) {\n    data += \"style:(\" + (el.styleBinding) + \"),\";\n  }\n  return data\n}\n\nvar style$1 = {\n  staticKeys: ['staticStyle'],\n  transformNode: transformNode$1,\n  genData: genData$1\n};\n\n/*  */\n\nvar decoder;\n\nvar he = {\n  decode: function decode (html) {\n    decoder = decoder || document.createElement('div');\n    decoder.innerHTML = html;\n    return decoder.textContent\n  }\n};\n\n/*  */\n\nvar isUnaryTag = makeMap(\n  'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +\n  'link,meta,param,source,track,wbr'\n);\n\n// Elements that you can, intentionally, leave open\n// (and which close themselves)\nvar canBeLeftOpenTag = makeMap(\n  'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source'\n);\n\n// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3\n// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content\nvar isNonPhrasingTag = makeMap(\n  'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +\n  'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +\n  'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +\n  'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +\n  'title,tr,track'\n);\n\n/**\n * Not type-checking this file because it's mostly vendor code.\n */\n\n/*!\n * HTML Parser By John Resig (ejohn.org)\n * Modified by Juriy \"kangax\" Zaytsev\n * Original code by Erik Arvidsson, Mozilla Public License\n * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js\n */\n\n// Regular Expressions for parsing tags and attributes\nvar attribute = /^\\s*([^\\s\"'<>\\/=]+)(?:\\s*(=)\\s*(?:\"([^\"]*)\"+|'([^']*)'+|([^\\s\"'=<>`]+)))?/;\n// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName\n// but for Vue templates we can enforce a simple charset\nvar ncname = '[a-zA-Z_][\\\\w\\\\-\\\\.]*';\nvar qnameCapture = \"((?:\" + ncname + \"\\\\:)?\" + ncname + \")\";\nvar startTagOpen = new RegExp((\"^<\" + qnameCapture));\nvar startTagClose = /^\\s*(\\/?)>/;\nvar endTag = new RegExp((\"^<\\\\/\" + qnameCapture + \"[^>]*>\"));\nvar doctype = /^<!DOCTYPE [^>]+>/i;\nvar comment = /^<!--/;\nvar conditionalComment = /^<!\\[/;\n\nvar IS_REGEX_CAPTURING_BROKEN = false;\n'x'.replace(/x(.)?/g, function (m, g) {\n  IS_REGEX_CAPTURING_BROKEN = g === '';\n});\n\n// Special Elements (can contain anything)\nvar isPlainTextElement = makeMap('script,style,textarea', true);\nvar reCache = {};\n\nvar decodingMap = {\n  '&lt;': '<',\n  '&gt;': '>',\n  '&quot;': '\"',\n  '&amp;': '&',\n  '&#10;': '\\n',\n  '&#9;': '\\t'\n};\nvar encodedAttr = /&(?:lt|gt|quot|amp);/g;\nvar encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g;\n\n// #5992\nvar isIgnoreNewlineTag = makeMap('pre,textarea', true);\nvar shouldIgnoreFirstNewline = function (tag, html) { return tag && isIgnoreNewlineTag(tag) && html[0] === '\\n'; };\n\nfunction decodeAttr (value, shouldDecodeNewlines) {\n  var re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;\n  return value.replace(re, function (match) { return decodingMap[match]; })\n}\n\nfunction parseHTML (html, options) {\n  var stack = [];\n  var expectHTML = options.expectHTML;\n  var isUnaryTag$$1 = options.isUnaryTag || no;\n  var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;\n  var index = 0;\n  var last, lastTag;\n  while (html) {\n    last = html;\n    // Make sure we're not in a plaintext content element like script/style\n    if (!lastTag || !isPlainTextElement(lastTag)) {\n      var textEnd = html.indexOf('<');\n      if (textEnd === 0) {\n        // Comment:\n        if (comment.test(html)) {\n          var commentEnd = html.indexOf('-->');\n\n          if (commentEnd >= 0) {\n            if (options.shouldKeepComment) {\n              options.comment(html.substring(4, commentEnd));\n            }\n            advance(commentEnd + 3);\n            continue\n          }\n        }\n\n        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment\n        if (conditionalComment.test(html)) {\n          var conditionalEnd = html.indexOf(']>');\n\n          if (conditionalEnd >= 0) {\n            advance(conditionalEnd + 2);\n            continue\n          }\n        }\n\n        // Doctype:\n        var doctypeMatch = html.match(doctype);\n        if (doctypeMatch) {\n          advance(doctypeMatch[0].length);\n          continue\n        }\n\n        // End tag:\n        var endTagMatch = html.match(endTag);\n        if (endTagMatch) {\n          var curIndex = index;\n          advance(endTagMatch[0].length);\n          parseEndTag(endTagMatch[1], curIndex, index);\n          continue\n        }\n\n        // Start tag:\n        var startTagMatch = parseStartTag();\n        if (startTagMatch) {\n          handleStartTag(startTagMatch);\n          if (shouldIgnoreFirstNewline(lastTag, html)) {\n            advance(1);\n          }\n          continue\n        }\n      }\n\n      var text = (void 0), rest = (void 0), next = (void 0);\n      if (textEnd >= 0) {\n        rest = html.slice(textEnd);\n        while (\n          !endTag.test(rest) &&\n          !startTagOpen.test(rest) &&\n          !comment.test(rest) &&\n          !conditionalComment.test(rest)\n        ) {\n          // < in plain text, be forgiving and treat it as text\n          next = rest.indexOf('<', 1);\n          if (next < 0) { break }\n          textEnd += next;\n          rest = html.slice(textEnd);\n        }\n        text = html.substring(0, textEnd);\n        advance(textEnd);\n      }\n\n      if (textEnd < 0) {\n        text = html;\n        html = '';\n      }\n\n      if (options.chars && text) {\n        options.chars(text);\n      }\n    } else {\n      var endTagLength = 0;\n      var stackedTag = lastTag.toLowerCase();\n      var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\\\s\\\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));\n      var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {\n        endTagLength = endTag.length;\n        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {\n          text = text\n            .replace(/<!--([\\s\\S]*?)-->/g, '$1')\n            .replace(/<!\\[CDATA\\[([\\s\\S]*?)]]>/g, '$1');\n        }\n        if (shouldIgnoreFirstNewline(stackedTag, text)) {\n          text = text.slice(1);\n        }\n        if (options.chars) {\n          options.chars(text);\n        }\n        return ''\n      });\n      index += html.length - rest$1.length;\n      html = rest$1;\n      parseEndTag(stackedTag, index - endTagLength, index);\n    }\n\n    if (html === last) {\n      options.chars && options.chars(html);\n      if (\"development\" !== 'production' && !stack.length && options.warn) {\n        options.warn((\"Mal-formatted tag at end of template: \\\"\" + html + \"\\\"\"));\n      }\n      break\n    }\n  }\n\n  // Clean up any remaining tags\n  parseEndTag();\n\n  function advance (n) {\n    index += n;\n    html = html.substring(n);\n  }\n\n  function parseStartTag () {\n    var start = html.match(startTagOpen);\n    if (start) {\n      var match = {\n        tagName: start[1],\n        attrs: [],\n        start: index\n      };\n      advance(start[0].length);\n      var end, attr;\n      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {\n        advance(attr[0].length);\n        match.attrs.push(attr);\n      }\n      if (end) {\n        match.unarySlash = end[1];\n        advance(end[0].length);\n        match.end = index;\n        return match\n      }\n    }\n  }\n\n  function handleStartTag (match) {\n    var tagName = match.tagName;\n    var unarySlash = match.unarySlash;\n\n    if (expectHTML) {\n      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {\n        parseEndTag(lastTag);\n      }\n      if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {\n        parseEndTag(tagName);\n      }\n    }\n\n    var unary = isUnaryTag$$1(tagName) || !!unarySlash;\n\n    var l = match.attrs.length;\n    var attrs = new Array(l);\n    for (var i = 0; i < l; i++) {\n      var args = match.attrs[i];\n      // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778\n      if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('\"\"') === -1) {\n        if (args[3] === '') { delete args[3]; }\n        if (args[4] === '') { delete args[4]; }\n        if (args[5] === '') { delete args[5]; }\n      }\n      var value = args[3] || args[4] || args[5] || '';\n      var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'\n        ? options.shouldDecodeNewlinesForHref\n        : options.shouldDecodeNewlines;\n      attrs[i] = {\n        name: args[1],\n        value: decodeAttr(value, shouldDecodeNewlines)\n      };\n    }\n\n    if (!unary) {\n      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs });\n      lastTag = tagName;\n    }\n\n    if (options.start) {\n      options.start(tagName, attrs, unary, match.start, match.end);\n    }\n  }\n\n  function parseEndTag (tagName, start, end) {\n    var pos, lowerCasedTagName;\n    if (start == null) { start = index; }\n    if (end == null) { end = index; }\n\n    if (tagName) {\n      lowerCasedTagName = tagName.toLowerCase();\n    }\n\n    // Find the closest opened tag of the same type\n    if (tagName) {\n      for (pos = stack.length - 1; pos >= 0; pos--) {\n        if (stack[pos].lowerCasedTag === lowerCasedTagName) {\n          break\n        }\n      }\n    } else {\n      // If no tag name is provided, clean shop\n      pos = 0;\n    }\n\n    if (pos >= 0) {\n      // Close all the open elements, up the stack\n      for (var i = stack.length - 1; i >= pos; i--) {\n        if (\"development\" !== 'production' &&\n          (i > pos || !tagName) &&\n          options.warn\n        ) {\n          options.warn(\n            (\"tag <\" + (stack[i].tag) + \"> has no matching end tag.\")\n          );\n        }\n        if (options.end) {\n          options.end(stack[i].tag, start, end);\n        }\n      }\n\n      // Remove the open elements from the stack\n      stack.length = pos;\n      lastTag = pos && stack[pos - 1].tag;\n    } else if (lowerCasedTagName === 'br') {\n      if (options.start) {\n        options.start(tagName, [], true, start, end);\n      }\n    } else if (lowerCasedTagName === 'p') {\n      if (options.start) {\n        options.start(tagName, [], false, start, end);\n      }\n      if (options.end) {\n        options.end(tagName, start, end);\n      }\n    }\n  }\n}\n\n/*  */\n\nvar onRE = /^@|^v-on:/;\nvar dirRE = /^v-|^@|^:/;\nvar forAliasRE = /(.*?)\\s+(?:in|of)\\s+(.*)/;\nvar forIteratorRE = /,([^,\\}\\]]*)(?:,([^,\\}\\]]*))?$/;\nvar stripParensRE = /^\\(|\\)$/g;\n\nvar argRE = /:(.*)$/;\nvar bindRE = /^:|^v-bind:/;\nvar modifierRE = /\\.[^.]+/g;\n\nvar decodeHTMLCached = cached(he.decode);\n\n// configurable state\nvar warn$2;\nvar delimiters;\nvar transforms;\nvar preTransforms;\nvar postTransforms;\nvar platformIsPreTag;\nvar platformMustUseProp;\nvar platformGetTagNamespace;\n\n\n\nfunction createASTElement (\n  tag,\n  attrs,\n  parent\n) {\n  return {\n    type: 1,\n    tag: tag,\n    attrsList: attrs,\n    attrsMap: makeAttrsMap(attrs),\n    parent: parent,\n    children: []\n  }\n}\n\n/**\n * Convert HTML string to AST.\n */\nfunction parse (\n  template,\n  options\n) {\n  warn$2 = options.warn || baseWarn;\n\n  platformIsPreTag = options.isPreTag || no;\n  platformMustUseProp = options.mustUseProp || no;\n  platformGetTagNamespace = options.getTagNamespace || no;\n\n  transforms = pluckModuleFunction(options.modules, 'transformNode');\n  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');\n  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');\n\n  delimiters = options.delimiters;\n\n  var stack = [];\n  var preserveWhitespace = options.preserveWhitespace !== false;\n  var root;\n  var currentParent;\n  var inVPre = false;\n  var inPre = false;\n  var warned = false;\n\n  function warnOnce (msg) {\n    if (!warned) {\n      warned = true;\n      warn$2(msg);\n    }\n  }\n\n  function endPre (element) {\n    // check pre state\n    if (element.pre) {\n      inVPre = false;\n    }\n    if (platformIsPreTag(element.tag)) {\n      inPre = false;\n    }\n  }\n\n  parseHTML(template, {\n    warn: warn$2,\n    expectHTML: options.expectHTML,\n    isUnaryTag: options.isUnaryTag,\n    canBeLeftOpenTag: options.canBeLeftOpenTag,\n    shouldDecodeNewlines: options.shouldDecodeNewlines,\n    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,\n    shouldKeepComment: options.comments,\n    start: function start (tag, attrs, unary) {\n      // check namespace.\n      // inherit parent ns if there is one\n      var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);\n\n      // handle IE svg bug\n      /* istanbul ignore if */\n      if (isIE && ns === 'svg') {\n        attrs = guardIESVGBug(attrs);\n      }\n\n      var element = createASTElement(tag, attrs, currentParent);\n      if (ns) {\n        element.ns = ns;\n      }\n\n      if (isForbiddenTag(element) && !isServerRendering()) {\n        element.forbidden = true;\n        \"development\" !== 'production' && warn$2(\n          'Templates should only be responsible for mapping the state to the ' +\n          'UI. Avoid placing tags with side-effects in your templates, such as ' +\n          \"<\" + tag + \">\" + ', as they will not be parsed.'\n        );\n      }\n\n      // apply pre-transforms\n      for (var i = 0; i < preTransforms.length; i++) {\n        element = preTransforms[i](element, options) || element;\n      }\n\n      if (!inVPre) {\n        processPre(element);\n        if (element.pre) {\n          inVPre = true;\n        }\n      }\n      if (platformIsPreTag(element.tag)) {\n        inPre = true;\n      }\n      if (inVPre) {\n        processRawAttrs(element);\n      } else if (!element.processed) {\n        // structural directives\n        processFor(element);\n        processIf(element);\n        processOnce(element);\n        // element-scope stuff\n        processElement(element, options);\n      }\n\n      function checkRootConstraints (el) {\n        {\n          if (el.tag === 'slot' || el.tag === 'template') {\n            warnOnce(\n              \"Cannot use <\" + (el.tag) + \"> as component root element because it may \" +\n              'contain multiple nodes.'\n            );\n          }\n          if (el.attrsMap.hasOwnProperty('v-for')) {\n            warnOnce(\n              'Cannot use v-for on stateful component root element because ' +\n              'it renders multiple elements.'\n            );\n          }\n        }\n      }\n\n      // tree management\n      if (!root) {\n        root = element;\n        checkRootConstraints(root);\n      } else if (!stack.length) {\n        // allow root elements with v-if, v-else-if and v-else\n        if (root.if && (element.elseif || element.else)) {\n          checkRootConstraints(element);\n          addIfCondition(root, {\n            exp: element.elseif,\n            block: element\n          });\n        } else {\n          warnOnce(\n            \"Component template should contain exactly one root element. \" +\n            \"If you are using v-if on multiple elements, \" +\n            \"use v-else-if to chain them instead.\"\n          );\n        }\n      }\n      if (currentParent && !element.forbidden) {\n        if (element.elseif || element.else) {\n          processIfConditions(element, currentParent);\n        } else if (element.slotScope) { // scoped slot\n          currentParent.plain = false;\n          var name = element.slotTarget || '\"default\"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;\n        } else {\n          currentParent.children.push(element);\n          element.parent = currentParent;\n        }\n      }\n      if (!unary) {\n        currentParent = element;\n        stack.push(element);\n      } else {\n        endPre(element);\n      }\n      // apply post-transforms\n      for (var i$1 = 0; i$1 < postTransforms.length; i$1++) {\n        postTransforms[i$1](element, options);\n      }\n    },\n\n    end: function end () {\n      // remove trailing whitespace\n      var element = stack[stack.length - 1];\n      var lastNode = element.children[element.children.length - 1];\n      if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {\n        element.children.pop();\n      }\n      // pop stack\n      stack.length -= 1;\n      currentParent = stack[stack.length - 1];\n      endPre(element);\n    },\n\n    chars: function chars (text) {\n      if (!currentParent) {\n        {\n          if (text === template) {\n            warnOnce(\n              'Component template requires a root element, rather than just text.'\n            );\n          } else if ((text = text.trim())) {\n            warnOnce(\n              (\"text \\\"\" + text + \"\\\" outside root element will be ignored.\")\n            );\n          }\n        }\n        return\n      }\n      // IE textarea placeholder bug\n      /* istanbul ignore if */\n      if (isIE &&\n        currentParent.tag === 'textarea' &&\n        currentParent.attrsMap.placeholder === text\n      ) {\n        return\n      }\n      var children = currentParent.children;\n      text = inPre || text.trim()\n        ? isTextTag(currentParent) ? text : decodeHTMLCached(text)\n        // only preserve whitespace if its not right after a starting tag\n        : preserveWhitespace && children.length ? ' ' : '';\n      if (text) {\n        var expression;\n        if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {\n          children.push({\n            type: 2,\n            expression: expression,\n            text: text\n          });\n        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {\n          children.push({\n            type: 3,\n            text: text\n          });\n        }\n      }\n    },\n    comment: function comment (text) {\n      currentParent.children.push({\n        type: 3,\n        text: text,\n        isComment: true\n      });\n    }\n  });\n  return root\n}\n\nfunction processPre (el) {\n  if (getAndRemoveAttr(el, 'v-pre') != null) {\n    el.pre = true;\n  }\n}\n\nfunction processRawAttrs (el) {\n  var l = el.attrsList.length;\n  if (l) {\n    var attrs = el.attrs = new Array(l);\n    for (var i = 0; i < l; i++) {\n      attrs[i] = {\n        name: el.attrsList[i].name,\n        value: JSON.stringify(el.attrsList[i].value)\n      };\n    }\n  } else if (!el.pre) {\n    // non root node in pre blocks with no attributes\n    el.plain = true;\n  }\n}\n\nfunction processElement (element, options) {\n  processKey(element);\n\n  // determine whether this is a plain element after\n  // removing structural attributes\n  element.plain = !element.key && !element.attrsList.length;\n\n  processRef(element);\n  processSlot(element);\n  processComponent(element);\n  for (var i = 0; i < transforms.length; i++) {\n    element = transforms[i](element, options) || element;\n  }\n  processAttrs(element);\n}\n\nfunction processKey (el) {\n  var exp = getBindingAttr(el, 'key');\n  if (exp) {\n    if (\"development\" !== 'production' && el.tag === 'template') {\n      warn$2(\"<template> cannot be keyed. Place the key on real elements instead.\");\n    }\n    el.key = exp;\n  }\n}\n\nfunction processRef (el) {\n  var ref = getBindingAttr(el, 'ref');\n  if (ref) {\n    el.ref = ref;\n    el.refInFor = checkInFor(el);\n  }\n}\n\nfunction processFor (el) {\n  var exp;\n  if ((exp = getAndRemoveAttr(el, 'v-for'))) {\n    var inMatch = exp.match(forAliasRE);\n    if (!inMatch) {\n      \"development\" !== 'production' && warn$2(\n        (\"Invalid v-for expression: \" + exp)\n      );\n      return\n    }\n    el.for = inMatch[2].trim();\n    var alias = inMatch[1].trim().replace(stripParensRE, '');\n    var iteratorMatch = alias.match(forIteratorRE);\n    if (iteratorMatch) {\n      el.alias = alias.replace(forIteratorRE, '');\n      el.iterator1 = iteratorMatch[1].trim();\n      if (iteratorMatch[2]) {\n        el.iterator2 = iteratorMatch[2].trim();\n      }\n    } else {\n      el.alias = alias;\n    }\n  }\n}\n\nfunction processIf (el) {\n  var exp = getAndRemoveAttr(el, 'v-if');\n  if (exp) {\n    el.if = exp;\n    addIfCondition(el, {\n      exp: exp,\n      block: el\n    });\n  } else {\n    if (getAndRemoveAttr(el, 'v-else') != null) {\n      el.else = true;\n    }\n    var elseif = getAndRemoveAttr(el, 'v-else-if');\n    if (elseif) {\n      el.elseif = elseif;\n    }\n  }\n}\n\nfunction processIfConditions (el, parent) {\n  var prev = findPrevElement(parent.children);\n  if (prev && prev.if) {\n    addIfCondition(prev, {\n      exp: el.elseif,\n      block: el\n    });\n  } else {\n    warn$2(\n      \"v-\" + (el.elseif ? ('else-if=\"' + el.elseif + '\"') : 'else') + \" \" +\n      \"used on element <\" + (el.tag) + \"> without corresponding v-if.\"\n    );\n  }\n}\n\nfunction findPrevElement (children) {\n  var i = children.length;\n  while (i--) {\n    if (children[i].type === 1) {\n      return children[i]\n    } else {\n      if (\"development\" !== 'production' && children[i].text !== ' ') {\n        warn$2(\n          \"text \\\"\" + (children[i].text.trim()) + \"\\\" between v-if and v-else(-if) \" +\n          \"will be ignored.\"\n        );\n      }\n      children.pop();\n    }\n  }\n}\n\nfunction addIfCondition (el, condition) {\n  if (!el.ifConditions) {\n    el.ifConditions = [];\n  }\n  el.ifConditions.push(condition);\n}\n\nfunction processOnce (el) {\n  var once$$1 = getAndRemoveAttr(el, 'v-once');\n  if (once$$1 != null) {\n    el.once = true;\n  }\n}\n\nfunction processSlot (el) {\n  if (el.tag === 'slot') {\n    el.slotName = getBindingAttr(el, 'name');\n    if (\"development\" !== 'production' && el.key) {\n      warn$2(\n        \"`key` does not work on <slot> because slots are abstract outlets \" +\n        \"and can possibly expand into multiple elements. \" +\n        \"Use the key on a wrapping element instead.\"\n      );\n    }\n  } else {\n    var slotScope;\n    if (el.tag === 'template') {\n      slotScope = getAndRemoveAttr(el, 'scope');\n      /* istanbul ignore if */\n      if (\"development\" !== 'production' && slotScope) {\n        warn$2(\n          \"the \\\"scope\\\" attribute for scoped slots have been deprecated and \" +\n          \"replaced by \\\"slot-scope\\\" since 2.5. The new \\\"slot-scope\\\" attribute \" +\n          \"can also be used on plain elements in addition to <template> to \" +\n          \"denote scoped slots.\",\n          true\n        );\n      }\n      el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');\n    } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {\n      /* istanbul ignore if */\n      if (\"development\" !== 'production' && el.attrsMap['v-for']) {\n        warn$2(\n          \"Ambiguous combined usage of slot-scope and v-for on <\" + (el.tag) + \"> \" +\n          \"(v-for takes higher priority). Use a wrapper <template> for the \" +\n          \"scoped slot to make it clearer.\",\n          true\n        );\n      }\n      el.slotScope = slotScope;\n    }\n    var slotTarget = getBindingAttr(el, 'slot');\n    if (slotTarget) {\n      el.slotTarget = slotTarget === '\"\"' ? '\"default\"' : slotTarget;\n      // preserve slot as an attribute for native shadow DOM compat\n      // only for non-scoped slots.\n      if (el.tag !== 'template' && !el.slotScope) {\n        addAttr(el, 'slot', slotTarget);\n      }\n    }\n  }\n}\n\nfunction processComponent (el) {\n  var binding;\n  if ((binding = getBindingAttr(el, 'is'))) {\n    el.component = binding;\n  }\n  if (getAndRemoveAttr(el, 'inline-template') != null) {\n    el.inlineTemplate = true;\n  }\n}\n\nfunction processAttrs (el) {\n  var list = el.attrsList;\n  var i, l, name, rawName, value, modifiers, isProp;\n  for (i = 0, l = list.length; i < l; i++) {\n    name = rawName = list[i].name;\n    value = list[i].value;\n    if (dirRE.test(name)) {\n      // mark element as dynamic\n      el.hasBindings = true;\n      // modifiers\n      modifiers = parseModifiers(name);\n      if (modifiers) {\n        name = name.replace(modifierRE, '');\n      }\n      if (bindRE.test(name)) { // v-bind\n        name = name.replace(bindRE, '');\n        value = parseFilters(value);\n        isProp = false;\n        if (modifiers) {\n          if (modifiers.prop) {\n            isProp = true;\n            name = camelize(name);\n            if (name === 'innerHtml') { name = 'innerHTML'; }\n          }\n          if (modifiers.camel) {\n            name = camelize(name);\n          }\n          if (modifiers.sync) {\n            addHandler(\n              el,\n              (\"update:\" + (camelize(name))),\n              genAssignmentCode(value, \"$event\")\n            );\n          }\n        }\n        if (isProp || (\n          !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)\n        )) {\n          addProp(el, name, value);\n        } else {\n          addAttr(el, name, value);\n        }\n      } else if (onRE.test(name)) { // v-on\n        name = name.replace(onRE, '');\n        addHandler(el, name, value, modifiers, false, warn$2);\n      } else { // normal directives\n        name = name.replace(dirRE, '');\n        // parse arg\n        var argMatch = name.match(argRE);\n        var arg = argMatch && argMatch[1];\n        if (arg) {\n          name = name.slice(0, -(arg.length + 1));\n        }\n        addDirective(el, name, rawName, value, arg, modifiers);\n        if (\"development\" !== 'production' && name === 'model') {\n          checkForAliasModel(el, value);\n        }\n      }\n    } else {\n      // literal attribute\n      {\n        var expression = parseText(value, delimiters);\n        if (expression) {\n          warn$2(\n            name + \"=\\\"\" + value + \"\\\": \" +\n            'Interpolation inside attributes has been removed. ' +\n            'Use v-bind or the colon shorthand instead. For example, ' +\n            'instead of <div id=\"{{ val }}\">, use <div :id=\"val\">.'\n          );\n        }\n      }\n      addAttr(el, name, JSON.stringify(value));\n      // #6887 firefox doesn't update muted state if set via attribute\n      // even immediately after element creation\n      if (!el.component &&\n          name === 'muted' &&\n          platformMustUseProp(el.tag, el.attrsMap.type, name)) {\n        addProp(el, name, 'true');\n      }\n    }\n  }\n}\n\nfunction checkInFor (el) {\n  var parent = el;\n  while (parent) {\n    if (parent.for !== undefined) {\n      return true\n    }\n    parent = parent.parent;\n  }\n  return false\n}\n\nfunction parseModifiers (name) {\n  var match = name.match(modifierRE);\n  if (match) {\n    var ret = {};\n    match.forEach(function (m) { ret[m.slice(1)] = true; });\n    return ret\n  }\n}\n\nfunction makeAttrsMap (attrs) {\n  var map = {};\n  for (var i = 0, l = attrs.length; i < l; i++) {\n    if (\n      \"development\" !== 'production' &&\n      map[attrs[i].name] && !isIE && !isEdge\n    ) {\n      warn$2('duplicate attribute: ' + attrs[i].name);\n    }\n    map[attrs[i].name] = attrs[i].value;\n  }\n  return map\n}\n\n// for script (e.g. type=\"x/template\") or style, do not decode content\nfunction isTextTag (el) {\n  return el.tag === 'script' || el.tag === 'style'\n}\n\nfunction isForbiddenTag (el) {\n  return (\n    el.tag === 'style' ||\n    (el.tag === 'script' && (\n      !el.attrsMap.type ||\n      el.attrsMap.type === 'text/javascript'\n    ))\n  )\n}\n\nvar ieNSBug = /^xmlns:NS\\d+/;\nvar ieNSPrefix = /^NS\\d+:/;\n\n/* istanbul ignore next */\nfunction guardIESVGBug (attrs) {\n  var res = [];\n  for (var i = 0; i < attrs.length; i++) {\n    var attr = attrs[i];\n    if (!ieNSBug.test(attr.name)) {\n      attr.name = attr.name.replace(ieNSPrefix, '');\n      res.push(attr);\n    }\n  }\n  return res\n}\n\nfunction checkForAliasModel (el, value) {\n  var _el = el;\n  while (_el) {\n    if (_el.for && _el.alias === value) {\n      warn$2(\n        \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\">: \" +\n        \"You are binding v-model directly to a v-for iteration alias. \" +\n        \"This will not be able to modify the v-for source array because \" +\n        \"writing to the alias is like modifying a function local variable. \" +\n        \"Consider using an array of objects and use v-model on an object property instead.\"\n      );\n    }\n    _el = _el.parent;\n  }\n}\n\n/*  */\n\n/**\n * Expand input[v-model] with dyanmic type bindings into v-if-else chains\n * Turn this:\n *   <input v-model=\"data[type]\" :type=\"type\">\n * into this:\n *   <input v-if=\"type === 'checkbox'\" type=\"checkbox\" v-model=\"data[type]\">\n *   <input v-else-if=\"type === 'radio'\" type=\"radio\" v-model=\"data[type]\">\n *   <input v-else :type=\"type\" v-model=\"data[type]\">\n */\n\nfunction preTransformNode (el, options) {\n  if (el.tag === 'input') {\n    var map = el.attrsMap;\n    if (map['v-model'] && (map['v-bind:type'] || map[':type'])) {\n      var typeBinding = getBindingAttr(el, 'type');\n      var ifCondition = getAndRemoveAttr(el, 'v-if', true);\n      var ifConditionExtra = ifCondition ? (\"&&(\" + ifCondition + \")\") : \"\";\n      var hasElse = getAndRemoveAttr(el, 'v-else', true) != null;\n      var elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true);\n      // 1. checkbox\n      var branch0 = cloneASTElement(el);\n      // process for on the main node\n      processFor(branch0);\n      addRawAttr(branch0, 'type', 'checkbox');\n      processElement(branch0, options);\n      branch0.processed = true; // prevent it from double-processed\n      branch0.if = \"(\" + typeBinding + \")==='checkbox'\" + ifConditionExtra;\n      addIfCondition(branch0, {\n        exp: branch0.if,\n        block: branch0\n      });\n      // 2. add radio else-if condition\n      var branch1 = cloneASTElement(el);\n      getAndRemoveAttr(branch1, 'v-for', true);\n      addRawAttr(branch1, 'type', 'radio');\n      processElement(branch1, options);\n      addIfCondition(branch0, {\n        exp: \"(\" + typeBinding + \")==='radio'\" + ifConditionExtra,\n        block: branch1\n      });\n      // 3. other\n      var branch2 = cloneASTElement(el);\n      getAndRemoveAttr(branch2, 'v-for', true);\n      addRawAttr(branch2, ':type', typeBinding);\n      processElement(branch2, options);\n      addIfCondition(branch0, {\n        exp: ifCondition,\n        block: branch2\n      });\n\n      if (hasElse) {\n        branch0.else = true;\n      } else if (elseIfCondition) {\n        branch0.elseif = elseIfCondition;\n      }\n\n      return branch0\n    }\n  }\n}\n\nfunction cloneASTElement (el) {\n  return createASTElement(el.tag, el.attrsList.slice(), el.parent)\n}\n\nfunction addRawAttr (el, name, value) {\n  el.attrsMap[name] = value;\n  el.attrsList.push({ name: name, value: value });\n}\n\nvar model$2 = {\n  preTransformNode: preTransformNode\n};\n\nvar modules$1 = [\n  klass$1,\n  style$1,\n  model$2\n];\n\n/*  */\n\nfunction text (el, dir) {\n  if (dir.value) {\n    addProp(el, 'textContent', (\"_s(\" + (dir.value) + \")\"));\n  }\n}\n\n/*  */\n\nfunction html (el, dir) {\n  if (dir.value) {\n    addProp(el, 'innerHTML', (\"_s(\" + (dir.value) + \")\"));\n  }\n}\n\nvar directives$1 = {\n  model: model,\n  text: text,\n  html: html\n};\n\n/*  */\n\nvar baseOptions = {\n  expectHTML: true,\n  modules: modules$1,\n  directives: directives$1,\n  isPreTag: isPreTag,\n  isUnaryTag: isUnaryTag,\n  mustUseProp: mustUseProp,\n  canBeLeftOpenTag: canBeLeftOpenTag,\n  isReservedTag: isReservedTag,\n  getTagNamespace: getTagNamespace,\n  staticKeys: genStaticKeys(modules$1)\n};\n\n/*  */\n\nvar isStaticKey;\nvar isPlatformReservedTag;\n\nvar genStaticKeysCached = cached(genStaticKeys$1);\n\n/**\n * Goal of the optimizer: walk the generated template AST tree\n * and detect sub-trees that are purely static, i.e. parts of\n * the DOM that never needs to change.\n *\n * Once we detect these sub-trees, we can:\n *\n * 1. Hoist them into constants, so that we no longer need to\n *    create fresh nodes for them on each re-render;\n * 2. Completely skip them in the patching process.\n */\nfunction optimize (root, options) {\n  if (!root) { return }\n  isStaticKey = genStaticKeysCached(options.staticKeys || '');\n  isPlatformReservedTag = options.isReservedTag || no;\n  // first pass: mark all non-static nodes.\n  markStatic$1(root);\n  // second pass: mark static roots.\n  markStaticRoots(root, false);\n}\n\nfunction genStaticKeys$1 (keys) {\n  return makeMap(\n    'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +\n    (keys ? ',' + keys : '')\n  )\n}\n\nfunction markStatic$1 (node) {\n  node.static = isStatic(node);\n  if (node.type === 1) {\n    // do not make component slot content static. this avoids\n    // 1. components not able to mutate slot nodes\n    // 2. static slot content fails for hot-reloading\n    if (\n      !isPlatformReservedTag(node.tag) &&\n      node.tag !== 'slot' &&\n      node.attrsMap['inline-template'] == null\n    ) {\n      return\n    }\n    for (var i = 0, l = node.children.length; i < l; i++) {\n      var child = node.children[i];\n      markStatic$1(child);\n      if (!child.static) {\n        node.static = false;\n      }\n    }\n    if (node.ifConditions) {\n      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {\n        var block = node.ifConditions[i$1].block;\n        markStatic$1(block);\n        if (!block.static) {\n          node.static = false;\n        }\n      }\n    }\n  }\n}\n\nfunction markStaticRoots (node, isInFor) {\n  if (node.type === 1) {\n    if (node.static || node.once) {\n      node.staticInFor = isInFor;\n    }\n    // For a node to qualify as a static root, it should have children that\n    // are not just static text. Otherwise the cost of hoisting out will\n    // outweigh the benefits and it's better off to just always render it fresh.\n    if (node.static && node.children.length && !(\n      node.children.length === 1 &&\n      node.children[0].type === 3\n    )) {\n      node.staticRoot = true;\n      return\n    } else {\n      node.staticRoot = false;\n    }\n    if (node.children) {\n      for (var i = 0, l = node.children.length; i < l; i++) {\n        markStaticRoots(node.children[i], isInFor || !!node.for);\n      }\n    }\n    if (node.ifConditions) {\n      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {\n        markStaticRoots(node.ifConditions[i$1].block, isInFor);\n      }\n    }\n  }\n}\n\nfunction isStatic (node) {\n  if (node.type === 2) { // expression\n    return false\n  }\n  if (node.type === 3) { // text\n    return true\n  }\n  return !!(node.pre || (\n    !node.hasBindings && // no dynamic bindings\n    !node.if && !node.for && // not v-if or v-for or v-else\n    !isBuiltInTag(node.tag) && // not a built-in\n    isPlatformReservedTag(node.tag) && // not a component\n    !isDirectChildOfTemplateFor(node) &&\n    Object.keys(node).every(isStaticKey)\n  ))\n}\n\nfunction isDirectChildOfTemplateFor (node) {\n  while (node.parent) {\n    node = node.parent;\n    if (node.tag !== 'template') {\n      return false\n    }\n    if (node.for) {\n      return true\n    }\n  }\n  return false\n}\n\n/*  */\n\nvar fnExpRE = /^\\s*([\\w$_]+|\\([^)]*?\\))\\s*=>|^function\\s*\\(/;\nvar simplePathRE = /^\\s*[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*|\\['.*?']|\\[\".*?\"]|\\[\\d+]|\\[[A-Za-z_$][\\w$]*])*\\s*$/;\n\n// keyCode aliases\nvar keyCodes = {\n  esc: 27,\n  tab: 9,\n  enter: 13,\n  space: 32,\n  up: 38,\n  left: 37,\n  right: 39,\n  down: 40,\n  'delete': [8, 46]\n};\n\n// #4868: modifiers that prevent the execution of the listener\n// need to explicitly return null so that we can determine whether to remove\n// the listener for .once\nvar genGuard = function (condition) { return (\"if(\" + condition + \")return null;\"); };\n\nvar modifierCode = {\n  stop: '$event.stopPropagation();',\n  prevent: '$event.preventDefault();',\n  self: genGuard(\"$event.target !== $event.currentTarget\"),\n  ctrl: genGuard(\"!$event.ctrlKey\"),\n  shift: genGuard(\"!$event.shiftKey\"),\n  alt: genGuard(\"!$event.altKey\"),\n  meta: genGuard(\"!$event.metaKey\"),\n  left: genGuard(\"'button' in $event && $event.button !== 0\"),\n  middle: genGuard(\"'button' in $event && $event.button !== 1\"),\n  right: genGuard(\"'button' in $event && $event.button !== 2\")\n};\n\nfunction genHandlers (\n  events,\n  isNative,\n  warn\n) {\n  var res = isNative ? 'nativeOn:{' : 'on:{';\n  for (var name in events) {\n    res += \"\\\"\" + name + \"\\\":\" + (genHandler(name, events[name])) + \",\";\n  }\n  return res.slice(0, -1) + '}'\n}\n\nfunction genHandler (\n  name,\n  handler\n) {\n  if (!handler) {\n    return 'function(){}'\n  }\n\n  if (Array.isArray(handler)) {\n    return (\"[\" + (handler.map(function (handler) { return genHandler(name, handler); }).join(',')) + \"]\")\n  }\n\n  var isMethodPath = simplePathRE.test(handler.value);\n  var isFunctionExpression = fnExpRE.test(handler.value);\n\n  if (!handler.modifiers) {\n    return isMethodPath || isFunctionExpression\n      ? handler.value\n      : (\"function($event){\" + (handler.value) + \"}\") // inline statement\n  } else {\n    var code = '';\n    var genModifierCode = '';\n    var keys = [];\n    for (var key in handler.modifiers) {\n      if (modifierCode[key]) {\n        genModifierCode += modifierCode[key];\n        // left/right\n        if (keyCodes[key]) {\n          keys.push(key);\n        }\n      } else if (key === 'exact') {\n        var modifiers = (handler.modifiers);\n        genModifierCode += genGuard(\n          ['ctrl', 'shift', 'alt', 'meta']\n            .filter(function (keyModifier) { return !modifiers[keyModifier]; })\n            .map(function (keyModifier) { return (\"$event.\" + keyModifier + \"Key\"); })\n            .join('||')\n        );\n      } else {\n        keys.push(key);\n      }\n    }\n    if (keys.length) {\n      code += genKeyFilter(keys);\n    }\n    // Make sure modifiers like prevent and stop get executed after key filtering\n    if (genModifierCode) {\n      code += genModifierCode;\n    }\n    var handlerCode = isMethodPath\n      ? handler.value + '($event)'\n      : isFunctionExpression\n        ? (\"(\" + (handler.value) + \")($event)\")\n        : handler.value;\n    return (\"function($event){\" + code + handlerCode + \"}\")\n  }\n}\n\nfunction genKeyFilter (keys) {\n  return (\"if(!('button' in $event)&&\" + (keys.map(genFilterCode).join('&&')) + \")return null;\")\n}\n\nfunction genFilterCode (key) {\n  var keyVal = parseInt(key, 10);\n  if (keyVal) {\n    return (\"$event.keyCode!==\" + keyVal)\n  }\n  var code = keyCodes[key];\n  return (\n    \"_k($event.keyCode,\" +\n    (JSON.stringify(key)) + \",\" +\n    (JSON.stringify(code)) + \",\" +\n    \"$event.key)\"\n  )\n}\n\n/*  */\n\nfunction on (el, dir) {\n  if (\"development\" !== 'production' && dir.modifiers) {\n    warn(\"v-on without argument does not support modifiers.\");\n  }\n  el.wrapListeners = function (code) { return (\"_g(\" + code + \",\" + (dir.value) + \")\"); };\n}\n\n/*  */\n\nfunction bind$1 (el, dir) {\n  el.wrapData = function (code) {\n    return (\"_b(\" + code + \",'\" + (el.tag) + \"',\" + (dir.value) + \",\" + (dir.modifiers && dir.modifiers.prop ? 'true' : 'false') + (dir.modifiers && dir.modifiers.sync ? ',true' : '') + \")\")\n  };\n}\n\n/*  */\n\nvar baseDirectives = {\n  on: on,\n  bind: bind$1,\n  cloak: noop\n};\n\n/*  */\n\nvar CodegenState = function CodegenState (options) {\n  this.options = options;\n  this.warn = options.warn || baseWarn;\n  this.transforms = pluckModuleFunction(options.modules, 'transformCode');\n  this.dataGenFns = pluckModuleFunction(options.modules, 'genData');\n  this.directives = extend(extend({}, baseDirectives), options.directives);\n  var isReservedTag = options.isReservedTag || no;\n  this.maybeComponent = function (el) { return !isReservedTag(el.tag); };\n  this.onceId = 0;\n  this.staticRenderFns = [];\n};\n\n\n\nfunction generate (\n  ast,\n  options\n) {\n  var state = new CodegenState(options);\n  var code = ast ? genElement(ast, state) : '_c(\"div\")';\n  return {\n    render: (\"with(this){return \" + code + \"}\"),\n    staticRenderFns: state.staticRenderFns\n  }\n}\n\nfunction genElement (el, state) {\n  if (el.staticRoot && !el.staticProcessed) {\n    return genStatic(el, state)\n  } else if (el.once && !el.onceProcessed) {\n    return genOnce(el, state)\n  } else if (el.for && !el.forProcessed) {\n    return genFor(el, state)\n  } else if (el.if && !el.ifProcessed) {\n    return genIf(el, state)\n  } else if (el.tag === 'template' && !el.slotTarget) {\n    return genChildren(el, state) || 'void 0'\n  } else if (el.tag === 'slot') {\n    return genSlot(el, state)\n  } else {\n    // component or element\n    var code;\n    if (el.component) {\n      code = genComponent(el.component, el, state);\n    } else {\n      var data = el.plain ? undefined : genData$2(el, state);\n\n      var children = el.inlineTemplate ? null : genChildren(el, state, true);\n      code = \"_c('\" + (el.tag) + \"'\" + (data ? (\",\" + data) : '') + (children ? (\",\" + children) : '') + \")\";\n    }\n    // module transforms\n    for (var i = 0; i < state.transforms.length; i++) {\n      code = state.transforms[i](el, code);\n    }\n    return code\n  }\n}\n\n// hoist static sub-trees out\nfunction genStatic (el, state) {\n  el.staticProcessed = true;\n  state.staticRenderFns.push((\"with(this){return \" + (genElement(el, state)) + \"}\"));\n  return (\"_m(\" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + \")\")\n}\n\n// v-once\nfunction genOnce (el, state) {\n  el.onceProcessed = true;\n  if (el.if && !el.ifProcessed) {\n    return genIf(el, state)\n  } else if (el.staticInFor) {\n    var key = '';\n    var parent = el.parent;\n    while (parent) {\n      if (parent.for) {\n        key = parent.key;\n        break\n      }\n      parent = parent.parent;\n    }\n    if (!key) {\n      \"development\" !== 'production' && state.warn(\n        \"v-once can only be used inside v-for that is keyed. \"\n      );\n      return genElement(el, state)\n    }\n    return (\"_o(\" + (genElement(el, state)) + \",\" + (state.onceId++) + \",\" + key + \")\")\n  } else {\n    return genStatic(el, state)\n  }\n}\n\nfunction genIf (\n  el,\n  state,\n  altGen,\n  altEmpty\n) {\n  el.ifProcessed = true; // avoid recursion\n  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)\n}\n\nfunction genIfConditions (\n  conditions,\n  state,\n  altGen,\n  altEmpty\n) {\n  if (!conditions.length) {\n    return altEmpty || '_e()'\n  }\n\n  var condition = conditions.shift();\n  if (condition.exp) {\n    return (\"(\" + (condition.exp) + \")?\" + (genTernaryExp(condition.block)) + \":\" + (genIfConditions(conditions, state, altGen, altEmpty)))\n  } else {\n    return (\"\" + (genTernaryExp(condition.block)))\n  }\n\n  // v-if with v-once should generate code like (a)?_m(0):_m(1)\n  function genTernaryExp (el) {\n    return altGen\n      ? altGen(el, state)\n      : el.once\n        ? genOnce(el, state)\n        : genElement(el, state)\n  }\n}\n\nfunction genFor (\n  el,\n  state,\n  altGen,\n  altHelper\n) {\n  var exp = el.for;\n  var alias = el.alias;\n  var iterator1 = el.iterator1 ? (\",\" + (el.iterator1)) : '';\n  var iterator2 = el.iterator2 ? (\",\" + (el.iterator2)) : '';\n\n  if (\"development\" !== 'production' &&\n    state.maybeComponent(el) &&\n    el.tag !== 'slot' &&\n    el.tag !== 'template' &&\n    !el.key\n  ) {\n    state.warn(\n      \"<\" + (el.tag) + \" v-for=\\\"\" + alias + \" in \" + exp + \"\\\">: component lists rendered with \" +\n      \"v-for should have explicit keys. \" +\n      \"See https://vuejs.org/guide/list.html#key for more info.\",\n      true /* tip */\n    );\n  }\n\n  el.forProcessed = true; // avoid recursion\n  return (altHelper || '_l') + \"((\" + exp + \"),\" +\n    \"function(\" + alias + iterator1 + iterator2 + \"){\" +\n      \"return \" + ((altGen || genElement)(el, state)) +\n    '})'\n}\n\nfunction genData$2 (el, state) {\n  var data = '{';\n\n  // directives first.\n  // directives may mutate the el's other properties before they are generated.\n  var dirs = genDirectives(el, state);\n  if (dirs) { data += dirs + ','; }\n\n  // key\n  if (el.key) {\n    data += \"key:\" + (el.key) + \",\";\n  }\n  // ref\n  if (el.ref) {\n    data += \"ref:\" + (el.ref) + \",\";\n  }\n  if (el.refInFor) {\n    data += \"refInFor:true,\";\n  }\n  // pre\n  if (el.pre) {\n    data += \"pre:true,\";\n  }\n  // record original tag name for components using \"is\" attribute\n  if (el.component) {\n    data += \"tag:\\\"\" + (el.tag) + \"\\\",\";\n  }\n  // module data generation functions\n  for (var i = 0; i < state.dataGenFns.length; i++) {\n    data += state.dataGenFns[i](el);\n  }\n  // attributes\n  if (el.attrs) {\n    data += \"attrs:{\" + (genProps(el.attrs)) + \"},\";\n  }\n  // DOM props\n  if (el.props) {\n    data += \"domProps:{\" + (genProps(el.props)) + \"},\";\n  }\n  // event handlers\n  if (el.events) {\n    data += (genHandlers(el.events, false, state.warn)) + \",\";\n  }\n  if (el.nativeEvents) {\n    data += (genHandlers(el.nativeEvents, true, state.warn)) + \",\";\n  }\n  // slot target\n  // only for non-scoped slots\n  if (el.slotTarget && !el.slotScope) {\n    data += \"slot:\" + (el.slotTarget) + \",\";\n  }\n  // scoped slots\n  if (el.scopedSlots) {\n    data += (genScopedSlots(el.scopedSlots, state)) + \",\";\n  }\n  // component v-model\n  if (el.model) {\n    data += \"model:{value:\" + (el.model.value) + \",callback:\" + (el.model.callback) + \",expression:\" + (el.model.expression) + \"},\";\n  }\n  // inline-template\n  if (el.inlineTemplate) {\n    var inlineTemplate = genInlineTemplate(el, state);\n    if (inlineTemplate) {\n      data += inlineTemplate + \",\";\n    }\n  }\n  data = data.replace(/,$/, '') + '}';\n  // v-bind data wrap\n  if (el.wrapData) {\n    data = el.wrapData(data);\n  }\n  // v-on data wrap\n  if (el.wrapListeners) {\n    data = el.wrapListeners(data);\n  }\n  return data\n}\n\nfunction genDirectives (el, state) {\n  var dirs = el.directives;\n  if (!dirs) { return }\n  var res = 'directives:[';\n  var hasRuntime = false;\n  var i, l, dir, needRuntime;\n  for (i = 0, l = dirs.length; i < l; i++) {\n    dir = dirs[i];\n    needRuntime = true;\n    var gen = state.directives[dir.name];\n    if (gen) {\n      // compile-time directive that manipulates AST.\n      // returns true if it also needs a runtime counterpart.\n      needRuntime = !!gen(el, dir, state.warn);\n    }\n    if (needRuntime) {\n      hasRuntime = true;\n      res += \"{name:\\\"\" + (dir.name) + \"\\\",rawName:\\\"\" + (dir.rawName) + \"\\\"\" + (dir.value ? (\",value:(\" + (dir.value) + \"),expression:\" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (\",arg:\\\"\" + (dir.arg) + \"\\\"\") : '') + (dir.modifiers ? (\",modifiers:\" + (JSON.stringify(dir.modifiers))) : '') + \"},\";\n    }\n  }\n  if (hasRuntime) {\n    return res.slice(0, -1) + ']'\n  }\n}\n\nfunction genInlineTemplate (el, state) {\n  var ast = el.children[0];\n  if (\"development\" !== 'production' && (\n    el.children.length !== 1 || ast.type !== 1\n  )) {\n    state.warn('Inline-template components must have exactly one child element.');\n  }\n  if (ast.type === 1) {\n    var inlineRenderFns = generate(ast, state.options);\n    return (\"inlineTemplate:{render:function(){\" + (inlineRenderFns.render) + \"},staticRenderFns:[\" + (inlineRenderFns.staticRenderFns.map(function (code) { return (\"function(){\" + code + \"}\"); }).join(',')) + \"]}\")\n  }\n}\n\nfunction genScopedSlots (\n  slots,\n  state\n) {\n  return (\"scopedSlots:_u([\" + (Object.keys(slots).map(function (key) {\n      return genScopedSlot(key, slots[key], state)\n    }).join(',')) + \"])\")\n}\n\nfunction genScopedSlot (\n  key,\n  el,\n  state\n) {\n  if (el.for && !el.forProcessed) {\n    return genForScopedSlot(key, el, state)\n  }\n  var fn = \"function(\" + (String(el.slotScope)) + \"){\" +\n    \"return \" + (el.tag === 'template'\n      ? el.if\n        ? ((el.if) + \"?\" + (genChildren(el, state) || 'undefined') + \":undefined\")\n        : genChildren(el, state) || 'undefined'\n      : genElement(el, state)) + \"}\";\n  return (\"{key:\" + key + \",fn:\" + fn + \"}\")\n}\n\nfunction genForScopedSlot (\n  key,\n  el,\n  state\n) {\n  var exp = el.for;\n  var alias = el.alias;\n  var iterator1 = el.iterator1 ? (\",\" + (el.iterator1)) : '';\n  var iterator2 = el.iterator2 ? (\",\" + (el.iterator2)) : '';\n  el.forProcessed = true; // avoid recursion\n  return \"_l((\" + exp + \"),\" +\n    \"function(\" + alias + iterator1 + iterator2 + \"){\" +\n      \"return \" + (genScopedSlot(key, el, state)) +\n    '})'\n}\n\nfunction genChildren (\n  el,\n  state,\n  checkSkip,\n  altGenElement,\n  altGenNode\n) {\n  var children = el.children;\n  if (children.length) {\n    var el$1 = children[0];\n    // optimize single v-for\n    if (children.length === 1 &&\n      el$1.for &&\n      el$1.tag !== 'template' &&\n      el$1.tag !== 'slot'\n    ) {\n      return (altGenElement || genElement)(el$1, state)\n    }\n    var normalizationType = checkSkip\n      ? getNormalizationType(children, state.maybeComponent)\n      : 0;\n    var gen = altGenNode || genNode;\n    return (\"[\" + (children.map(function (c) { return gen(c, state); }).join(',')) + \"]\" + (normalizationType ? (\",\" + normalizationType) : ''))\n  }\n}\n\n// determine the normalization needed for the children array.\n// 0: no normalization needed\n// 1: simple normalization needed (possible 1-level deep nested array)\n// 2: full normalization needed\nfunction getNormalizationType (\n  children,\n  maybeComponent\n) {\n  var res = 0;\n  for (var i = 0; i < children.length; i++) {\n    var el = children[i];\n    if (el.type !== 1) {\n      continue\n    }\n    if (needsNormalization(el) ||\n        (el.ifConditions && el.ifConditions.some(function (c) { return needsNormalization(c.block); }))) {\n      res = 2;\n      break\n    }\n    if (maybeComponent(el) ||\n        (el.ifConditions && el.ifConditions.some(function (c) { return maybeComponent(c.block); }))) {\n      res = 1;\n    }\n  }\n  return res\n}\n\nfunction needsNormalization (el) {\n  return el.for !== undefined || el.tag === 'template' || el.tag === 'slot'\n}\n\nfunction genNode (node, state) {\n  if (node.type === 1) {\n    return genElement(node, state)\n  } if (node.type === 3 && node.isComment) {\n    return genComment(node)\n  } else {\n    return genText(node)\n  }\n}\n\nfunction genText (text) {\n  return (\"_v(\" + (text.type === 2\n    ? text.expression // no need for () because already wrapped in _s()\n    : transformSpecialNewlines(JSON.stringify(text.text))) + \")\")\n}\n\nfunction genComment (comment) {\n  return (\"_e(\" + (JSON.stringify(comment.text)) + \")\")\n}\n\nfunction genSlot (el, state) {\n  var slotName = el.slotName || '\"default\"';\n  var children = genChildren(el, state);\n  var res = \"_t(\" + slotName + (children ? (\",\" + children) : '');\n  var attrs = el.attrs && (\"{\" + (el.attrs.map(function (a) { return ((camelize(a.name)) + \":\" + (a.value)); }).join(',')) + \"}\");\n  var bind$$1 = el.attrsMap['v-bind'];\n  if ((attrs || bind$$1) && !children) {\n    res += \",null\";\n  }\n  if (attrs) {\n    res += \",\" + attrs;\n  }\n  if (bind$$1) {\n    res += (attrs ? '' : ',null') + \",\" + bind$$1;\n  }\n  return res + ')'\n}\n\n// componentName is el.component, take it as argument to shun flow's pessimistic refinement\nfunction genComponent (\n  componentName,\n  el,\n  state\n) {\n  var children = el.inlineTemplate ? null : genChildren(el, state, true);\n  return (\"_c(\" + componentName + \",\" + (genData$2(el, state)) + (children ? (\",\" + children) : '') + \")\")\n}\n\nfunction genProps (props) {\n  var res = '';\n  for (var i = 0; i < props.length; i++) {\n    var prop = props[i];\n    res += \"\\\"\" + (prop.name) + \"\\\":\" + (transformSpecialNewlines(prop.value)) + \",\";\n  }\n  return res.slice(0, -1)\n}\n\n// #3895, #4268\nfunction transformSpecialNewlines (text) {\n  return text\n    .replace(/\\u2028/g, '\\\\u2028')\n    .replace(/\\u2029/g, '\\\\u2029')\n}\n\n/*  */\n\n// these keywords should not appear inside expressions, but operators like\n// typeof, instanceof and in are allowed\nvar prohibitedKeywordRE = new RegExp('\\\\b' + (\n  'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' +\n  'super,throw,while,yield,delete,export,import,return,switch,default,' +\n  'extends,finally,continue,debugger,function,arguments'\n).split(',').join('\\\\b|\\\\b') + '\\\\b');\n\n// these unary operators should not be used as property/method names\nvar unaryOperatorsRE = new RegExp('\\\\b' + (\n  'delete,typeof,void'\n).split(',').join('\\\\s*\\\\([^\\\\)]*\\\\)|\\\\b') + '\\\\s*\\\\([^\\\\)]*\\\\)');\n\n// strip strings in expressions\nvar stripStringRE = /'(?:[^'\\\\]|\\\\.)*'|\"(?:[^\"\\\\]|\\\\.)*\"|`(?:[^`\\\\]|\\\\.)*\\$\\{|\\}(?:[^`\\\\]|\\\\.)*`|`(?:[^`\\\\]|\\\\.)*`/g;\n\n// detect problematic expressions in a template\nfunction detectErrors (ast) {\n  var errors = [];\n  if (ast) {\n    checkNode(ast, errors);\n  }\n  return errors\n}\n\nfunction checkNode (node, errors) {\n  if (node.type === 1) {\n    for (var name in node.attrsMap) {\n      if (dirRE.test(name)) {\n        var value = node.attrsMap[name];\n        if (value) {\n          if (name === 'v-for') {\n            checkFor(node, (\"v-for=\\\"\" + value + \"\\\"\"), errors);\n          } else if (onRE.test(name)) {\n            checkEvent(value, (name + \"=\\\"\" + value + \"\\\"\"), errors);\n          } else {\n            checkExpression(value, (name + \"=\\\"\" + value + \"\\\"\"), errors);\n          }\n        }\n      }\n    }\n    if (node.children) {\n      for (var i = 0; i < node.children.length; i++) {\n        checkNode(node.children[i], errors);\n      }\n    }\n  } else if (node.type === 2) {\n    checkExpression(node.expression, node.text, errors);\n  }\n}\n\nfunction checkEvent (exp, text, errors) {\n  var stipped = exp.replace(stripStringRE, '');\n  var keywordMatch = stipped.match(unaryOperatorsRE);\n  if (keywordMatch && stipped.charAt(keywordMatch.index - 1) !== '$') {\n    errors.push(\n      \"avoid using JavaScript unary operator as property name: \" +\n      \"\\\"\" + (keywordMatch[0]) + \"\\\" in expression \" + (text.trim())\n    );\n  }\n  checkExpression(exp, text, errors);\n}\n\nfunction checkFor (node, text, errors) {\n  checkExpression(node.for || '', text, errors);\n  checkIdentifier(node.alias, 'v-for alias', text, errors);\n  checkIdentifier(node.iterator1, 'v-for iterator', text, errors);\n  checkIdentifier(node.iterator2, 'v-for iterator', text, errors);\n}\n\nfunction checkIdentifier (\n  ident,\n  type,\n  text,\n  errors\n) {\n  if (typeof ident === 'string') {\n    try {\n      new Function((\"var \" + ident + \"=_\"));\n    } catch (e) {\n      errors.push((\"invalid \" + type + \" \\\"\" + ident + \"\\\" in expression: \" + (text.trim())));\n    }\n  }\n}\n\nfunction checkExpression (exp, text, errors) {\n  try {\n    new Function((\"return \" + exp));\n  } catch (e) {\n    var keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE);\n    if (keywordMatch) {\n      errors.push(\n        \"avoid using JavaScript keyword as property name: \" +\n        \"\\\"\" + (keywordMatch[0]) + \"\\\"\\n  Raw expression: \" + (text.trim())\n      );\n    } else {\n      errors.push(\n        \"invalid expression: \" + (e.message) + \" in\\n\\n\" +\n        \"    \" + exp + \"\\n\\n\" +\n        \"  Raw expression: \" + (text.trim()) + \"\\n\"\n      );\n    }\n  }\n}\n\n/*  */\n\nfunction createFunction (code, errors) {\n  try {\n    return new Function(code)\n  } catch (err) {\n    errors.push({ err: err, code: code });\n    return noop\n  }\n}\n\nfunction createCompileToFunctionFn (compile) {\n  var cache = Object.create(null);\n\n  return function compileToFunctions (\n    template,\n    options,\n    vm\n  ) {\n    options = extend({}, options);\n    var warn$$1 = options.warn || warn;\n    delete options.warn;\n\n    /* istanbul ignore if */\n    {\n      // detect possible CSP restriction\n      try {\n        new Function('return 1');\n      } catch (e) {\n        if (e.toString().match(/unsafe-eval|CSP/)) {\n          warn$$1(\n            'It seems you are using the standalone build of Vue.js in an ' +\n            'environment with Content Security Policy that prohibits unsafe-eval. ' +\n            'The template compiler cannot work in this environment. Consider ' +\n            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +\n            'templates into render functions.'\n          );\n        }\n      }\n    }\n\n    // check cache\n    var key = options.delimiters\n      ? String(options.delimiters) + template\n      : template;\n    if (cache[key]) {\n      return cache[key]\n    }\n\n    // compile\n    var compiled = compile(template, options);\n\n    // check compilation errors/tips\n    {\n      if (compiled.errors && compiled.errors.length) {\n        warn$$1(\n          \"Error compiling template:\\n\\n\" + template + \"\\n\\n\" +\n          compiled.errors.map(function (e) { return (\"- \" + e); }).join('\\n') + '\\n',\n          vm\n        );\n      }\n      if (compiled.tips && compiled.tips.length) {\n        compiled.tips.forEach(function (msg) { return tip(msg, vm); });\n      }\n    }\n\n    // turn code into functions\n    var res = {};\n    var fnGenErrors = [];\n    res.render = createFunction(compiled.render, fnGenErrors);\n    res.staticRenderFns = compiled.staticRenderFns.map(function (code) {\n      return createFunction(code, fnGenErrors)\n    });\n\n    // check function generation errors.\n    // this should only happen if there is a bug in the compiler itself.\n    // mostly for codegen development use\n    /* istanbul ignore if */\n    {\n      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {\n        warn$$1(\n          \"Failed to generate render function:\\n\\n\" +\n          fnGenErrors.map(function (ref) {\n            var err = ref.err;\n            var code = ref.code;\n\n            return ((err.toString()) + \" in\\n\\n\" + code + \"\\n\");\n        }).join('\\n'),\n          vm\n        );\n      }\n    }\n\n    return (cache[key] = res)\n  }\n}\n\n/*  */\n\nfunction createCompilerCreator (baseCompile) {\n  return function createCompiler (baseOptions) {\n    function compile (\n      template,\n      options\n    ) {\n      var finalOptions = Object.create(baseOptions);\n      var errors = [];\n      var tips = [];\n      finalOptions.warn = function (msg, tip) {\n        (tip ? tips : errors).push(msg);\n      };\n\n      if (options) {\n        // merge custom modules\n        if (options.modules) {\n          finalOptions.modules =\n            (baseOptions.modules || []).concat(options.modules);\n        }\n        // merge custom directives\n        if (options.directives) {\n          finalOptions.directives = extend(\n            Object.create(baseOptions.directives),\n            options.directives\n          );\n        }\n        // copy other options\n        for (var key in options) {\n          if (key !== 'modules' && key !== 'directives') {\n            finalOptions[key] = options[key];\n          }\n        }\n      }\n\n      var compiled = baseCompile(template, finalOptions);\n      {\n        errors.push.apply(errors, detectErrors(compiled.ast));\n      }\n      compiled.errors = errors;\n      compiled.tips = tips;\n      return compiled\n    }\n\n    return {\n      compile: compile,\n      compileToFunctions: createCompileToFunctionFn(compile)\n    }\n  }\n}\n\n/*  */\n\n// `createCompilerCreator` allows creating compilers that use alternative\n// parser/optimizer/codegen, e.g the SSR optimizing compiler.\n// Here we just export a default compiler using the default parts.\nvar createCompiler = createCompilerCreator(function baseCompile (\n  template,\n  options\n) {\n  var ast = parse(template.trim(), options);\n  optimize(ast, options);\n  var code = generate(ast, options);\n  return {\n    ast: ast,\n    render: code.render,\n    staticRenderFns: code.staticRenderFns\n  }\n});\n\n/*  */\n\nvar ref$1 = createCompiler(baseOptions);\nvar compileToFunctions = ref$1.compileToFunctions;\n\n/*  */\n\n// check whether current browser encodes a char inside attribute values\nvar div;\nfunction getShouldDecode (href) {\n  div = div || document.createElement('div');\n  div.innerHTML = href ? \"<a href=\\\"\\n\\\"/>\" : \"<div a=\\\"\\n\\\"/>\";\n  return div.innerHTML.indexOf('&#10;') > 0\n}\n\n// #3663: IE encodes newlines inside attribute values while other browsers don't\nvar shouldDecodeNewlines = inBrowser ? getShouldDecode(false) : false;\n// #6828: chrome encodes content in a[href]\nvar shouldDecodeNewlinesForHref = inBrowser ? getShouldDecode(true) : false;\n\n/*  */\n\nvar idToTemplate = cached(function (id) {\n  var el = query(id);\n  return el && el.innerHTML\n});\n\nvar mount = Vue$3.prototype.$mount;\nVue$3.prototype.$mount = function (\n  el,\n  hydrating\n) {\n  el = el && query(el);\n\n  /* istanbul ignore if */\n  if (el === document.body || el === document.documentElement) {\n    \"development\" !== 'production' && warn(\n      \"Do not mount Vue to <html> or <body> - mount to normal elements instead.\"\n    );\n    return this\n  }\n\n  var options = this.$options;\n  // resolve template/el and convert to render function\n  if (!options.render) {\n    var template = options.template;\n    if (template) {\n      if (typeof template === 'string') {\n        if (template.charAt(0) === '#') {\n          template = idToTemplate(template);\n          /* istanbul ignore if */\n          if (\"development\" !== 'production' && !template) {\n            warn(\n              (\"Template element not found or is empty: \" + (options.template)),\n              this\n            );\n          }\n        }\n      } else if (template.nodeType) {\n        template = template.innerHTML;\n      } else {\n        {\n          warn('invalid template option:' + template, this);\n        }\n        return this\n      }\n    } else if (el) {\n      template = getOuterHTML(el);\n    }\n    if (template) {\n      /* istanbul ignore if */\n      if (\"development\" !== 'production' && config.performance && mark) {\n        mark('compile');\n      }\n\n      var ref = compileToFunctions(template, {\n        shouldDecodeNewlines: shouldDecodeNewlines,\n        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,\n        delimiters: options.delimiters,\n        comments: options.comments\n      }, this);\n      var render = ref.render;\n      var staticRenderFns = ref.staticRenderFns;\n      options.render = render;\n      options.staticRenderFns = staticRenderFns;\n\n      /* istanbul ignore if */\n      if (\"development\" !== 'production' && config.performance && mark) {\n        mark('compile end');\n        measure((\"vue \" + (this._name) + \" compile\"), 'compile', 'compile end');\n      }\n    }\n  }\n  return mount.call(this, el, hydrating)\n};\n\n/**\n * Get outerHTML of elements, taking care\n * of SVG elements in IE as well.\n */\nfunction getOuterHTML (el) {\n  if (el.outerHTML) {\n    return el.outerHTML\n  } else {\n    var container = document.createElement('div');\n    container.appendChild(el.cloneNode(true));\n    return container.innerHTML\n  }\n}\n\nVue$3.compile = compileToFunctions;\n\nreturn Vue$3;\n\n})));\n"
  },
  {
    "path": "commons-api2doc/src/main/resources/templates/api2doc/doc.ftl",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<#if title ??>\n    <title>${title}</title>\n</#if>\n    <!--\n        使用 rem 布局，使 H5 页面能适配不同设备屏幕尺寸\n        flexible-lite-1.0.js 用于计算 html 根元素的 font-size 大小\n        然后 css 或 less 中所有的尺寸值一定要用 rem 单位，而不是 px 或其它单位。\n    -->\n    <meta charset=\"UTF-8\" name=\"viewport\"\n          content=\"width=device-width,initial-scale=1,user-scalable=0\"/>\n    <script src=\"/website/flexible-lite/flexible-lite-1.0.js\"></script>\n    <script type=\"text/javascript\">\n        flex(1000);\n    </script>\n    <!--\n            务必确保在 less.js 之前加载你的样式表。\n            如果加载多个 .less 样式表文件，每个文件都会被单独编译。\n            因此，一个文件中所定义的任何变量、mixin 或命名空间都无法在其它文件中访问。\n    -->\n    <link rel=\"stylesheet/less\" type=\"text/css\" href=\"/api2doc/css/doc.less?v=${v}\">\n    <link rel=\"stylesheet/less\" type=\"text/css\" href=\"/api2doc/css/md.less?v=${v}\">\n    <script type=\"text/javascript\" src=\"/website/less/less-1.7.0.js\"></script>\n\n    <!-- 使代码根据语法显示样式（如：高亮等） -->\n    <link rel=\"stylesheet\"\n          href=\"//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css\">\n    <script src=\"//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js\"></script>\n    <script type=\"text/javascript\">\n        hljs.initHighlightingOnLoad();\n    </script>\n\n</head>\n<body>\n<div class=\"test-entry\">\n    <a href=\"/api2doc/test.html?v=${v}\">接口测试</a>\n</div>\n<div class=\"clear\"/>\n<#if title ??>\n<div class=\"title\">${title}</div>\n</#if>\n<div class=\"content\">${content}</div>\n</body>\n</html>"
  },
  {
    "path": "commons-api2doc/src/main/resources/templates/api2doc/home.ftl",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <title>${title}</title>\n    <!--\n        使用 rem 布局，使 H5 页面能适配不同设备屏幕尺寸\n        flexible-lite-1.0.js 用于计算 html 根元素的 font-size 大小\n        然后 css 或 less 中所有的尺寸值一定要用 rem 单位，而不是 px 或其它单位。\n    -->\n    <meta charset=\"UTF-8\" name=\"viewport\"\n          content=\"width=device-width,initial-scale=1,user-scalable=0\"/>\n    <script src=\"/website/flexible-lite/flexible-lite-1.0.js\"></script>\n    <script type=\"text/javascript\">\n        flex(1000);\n    </script>\n\n    <!-- 左侧菜单是用 element-ui 写的，element-ui 又用了 vue，因此都要引用 -->\n    <link href=\"/website/element-ui/element-ui.min.css\" rel=\"stylesheet\">\n    <script src=\"/website/vue/vue-2.5.10.min.js\"></script>\n    <script src=\"/website/element-ui/element-ui.min.js\"></script>\n\n    <!-- 使用 jQuery 操作，让菜单栏始终悬浮出现在屏幕上。 -->\n    <script src=\"/website/jquery/jquery-3.3.1.min.js\"></script>\n\n    <!--\n            务必确保在 less.js 之前加载你的样式表。\n            如果加载多个 .less 样式表文件，每个文件都会被单独编译。\n            因此，一个文件中所定义的任何变量、mixin 或命名空间都无法在其它文件中访问。\n    -->\n    <link href=\"/api2doc/css/home.less?v=${v}\" rel=\"stylesheet/less\" type=\"text/css\">\n    <script src=\"/website/less/less-3.0.0.min.js\" type=\"text/javascript\"></script>\n</head>\n<body>\n<div id=\"app\" class=\"doc-app\" v-loading.fullscreen.lock=\"loading\"\n     element-loading-text=\"拼命加载中...\">\n\n    <div class=\"doc-top\">\n    <#if icon??>\n        <div class=\"doc-icon\">\n            <img class=\"doc-icon-img\" src=\"${icon}\"\n                 onclick=\"location.href='/api2doc/home.html';\">\n        </div>\n    </#if>\n        <div class=\"doc-title\">\n            <span>${title}</span>\n        </div>\n        <div class=\"doc-end\"></div>\n    </div>\n\n    <div class=\"doc-body\">\n\n        <!-- 左侧栏菜单 -->\n        <div class=\"doc-left\">\n            <div id=\"doc-menus\">\n                <el-menu class=\"el-menu-vertical-demo\" theme=\"light\"\n                         default-active=\"${p}\">\n                <#list menus as folder>\n                    <el-submenu index=\"${folder.index}\">\n                        <template slot=\"title\">${folder.name}</template>\n                        <#list folder.children as doc>\n                            <el-menu-item index=\"${doc.index}\">\n                                <a href=\"${doc.url}\" target=\"_self\">${doc.name}</a>\n                            </el-menu-item>\n                        </#list>\n                    </el-submenu>\n                </#list>\n                </el-menu>\n            </div>\n        </div>\n\n        <!-- 左侧菜单栏与右侧内容区域的分隔 -->\n        <div class=\"doc-split\"></div>\n\n        <!-- 页面内容区域 -->\n        <div class=\"doc-content\">\n            <iframe id=\"doc-frame-id\" name=\"doc-frame-name\" class=\"doc-frame\"\n                    scrolling=\"yes\" frameborder=\"0\" seamless=\"seamless\"\n                    src=\"${docPath}\"></iframe>\n        </div>\n\n        <div class=\"doc-end\"></div>\n    </div>\n</div>\n<script type=\"text/javascript\">\n\n    // 如果网速不给力导致加载太慢时，\n    // 会让页面先出现一个“拼命加载中...”的提示，\n    // 加载完成后再显示文档内容。\n    new Vue({\n        el: '#app',\n        data: function () {\n            return {\n                loading: true\n            }\n        },\n        methods: {},\n        created: function () {\n            var _self = this;\n            _self.loading = false;\n        }\n    });\n</script>\n</body>\n</html>"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/Api2DocDemoApp.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.config.EnableApi2Doc;\nimport com.terran4j.commons.restpack.EnableRestPack;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.ApplicationContext;\n\n//  文档访问地址：\n//  http://localhost:8080/api2doc/home.html\n//  API 元数据\n//  http://localhost:8080/api2doc/meta/classes\n@EnableApi2Doc\n@EnableRestPack\n@SpringBootApplication\npublic class Api2DocDemoApp {\n\n    public static void main(String[] args) {\n        ApplicationContext context = SpringApplication.run(Api2DocDemoApp.class, args);\n        CodeGenerator.genAndroidCode(context);\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/CodeGenerator.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.codewriter.CodeConfig;\nimport com.terran4j.commons.api2doc.codewriter.FileCodeOutput;\nimport com.terran4j.commons.api2doc.codewriter.RetrofitCodeWriter;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocService;\nimport org.springframework.context.ApplicationContext;\n\nimport java.io.File;\nimport java.util.List;\n\npublic class CodeGenerator {\n\n\n    public static void genAndroidCode(ApplicationContext context) {\n        Api2DocService api2DocService = context.getBean(Api2DocService.class);\n        List<ApiFolderObject> folders = api2DocService.getFolders();\n\n        RetrofitCodeWriter writer = context.getBean(RetrofitCodeWriter.class);\n        String folderPath = System.getProperty(\"java.io.tmpdir\");\n        folderPath = folderPath.replaceAll(\"\\\\\\\\\", \"/\");\n        if (!folderPath.endsWith(\"/\")) {\n            folderPath = folderPath + \"/\";\n        }\n        folderPath = folderPath + \"yike-code\";\n        File dir = new File(folderPath);\n        if (dir.isDirectory()) {\n            dir.delete();\n        }\n        dir.mkdir();\n        System.out.println(\"gen doc to folder: \" + folderPath);\n        FileCodeOutput out = new FileCodeOutput(folderPath);\n\n        CodeConfig config = new CodeConfig() {\n\n            public List<ApiParamObject> getExtraPrams(ApiDocObject doc) {\n                return null;\n            }\n        };\n        config.setPkgName(\"com.terran4j.api2doc.demo\");\n        config.setDeclaredComment(\"本类由 Api2Doc 自动生成，建议您不要修改它，\" //\n                + \"以便更新时重新生并覆盖掉之前代码即可。\");\n\n        writer.writeCode(folders, out, config);\n        System.out.println(\"gen doc done\");\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/FileInfo.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.util.Strings;\nimport org.springframework.web.multipart.MultipartFile;\n\nimport java.io.IOException;\n\npublic class FileInfo {\n\n    private String name;\n\n    private String content;\n\n    private String msg;\n\n    public FileInfo() {\n    }\n\n    public static FileInfo parse(MultipartFile file, String name) throws IOException {\n        String fileName = file.getOriginalFilename();\n        String content = Strings.getString(file.getInputStream());\n        String msg = \"requestPart, file = \" + fileName;\n        return new FileInfo(name, content, msg);\n    }\n\n    public FileInfo(String name, String content, String msg) {\n        this.name = name;\n        this.content = content;\n        this.msg = msg;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getContent() {\n        return content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/ShowMappingController.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.restpack.RestPackController;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.bind.annotation.*;\n\n/**\n *\n */\n@Api2Doc(value = \"mapping\", name = \"演示各种Mapping生成文档\", order = 100)\n@RestPackController\n@RequestMapping(value = \"/api2doc/demo/mapping\")\npublic class ShowMappingController {\n\n    private static final Logger log = LoggerFactory.getLogger(ShowMappingController.class);\n\n    @ApiComment(value = \"返回 doRequest 的消息文本\", sample = \"doRequest, id = 123\")\n    @RequestMapping(value = \"/doRequest/{id}\", name = \"演示 @RequestMapping 方法\")\n    public String doRequest(\n            @ApiComment(\"用户id\") @PathVariable(\"id\") Long id) {\n        if (log.isInfoEnabled()) {\n            log.info(\"doRequest, id = {}\", id);\n        }\n        return \"doRequest, id = \" + id;\n    }\n\n    @GetMapping(value = \"/doGet/{id}\", name = \"演示 @GetMapping 方法\")\n    public String doGet(\n            @ApiComment(\"用户id\") @PathVariable(\"id\") Long id) {\n        if (log.isInfoEnabled()) {\n            log.info(\"doGet, id = {}\", id);\n        }\n        return \"doGet, id = \" + id;\n    }\n\n    @PostMapping(value = \"/doPost/{id}\", name = \"演示 @PostMapping 方法\")\n    public String doPost(\n            @ApiComment(\"用户id\") @PathVariable(\"id\") Long id,\n            @ApiComment(\"用户描述\") String desc) {\n        if (log.isInfoEnabled()) {\n            log.info(\"doPost, id = {}, desc = {}\", id, desc);\n        }\n        return \"doPost, id = \" + id + \", desc = \" + desc;\n    }\n\n    @PutMapping(value = \"/doPut/{id}\", name = \"演示 @PutMapping 方法\")\n    public String doPut(\n            @ApiComment(\"用户id\") @PathVariable(\"id\") Long id,\n            @ApiComment(\"用户描述\") String desc) {\n        if (log.isInfoEnabled()) {\n            log.info(\"doPut, id = {}, desc = {}\", id, desc);\n        }\n        return \"doPut, id = \" + id + \", desc = \" + desc;\n    }\n\n    @DeleteMapping(value = \"/doDelete/{id}\", name = \"演示 @DeleteMapping 方法\")\n    public String doDelete(\n            @ApiComment(\"用户id\") @PathVariable(\"id\") Long id) {\n        if (log.isInfoEnabled()) {\n            log.info(\"doDelete, id = {}\", id);\n        }\n        return \"doDelete, id = \" + id;\n    }\n\n    @PatchMapping(value = \"/doPatch/{id}\", name = \"演示 @PatchMapping 方法\")\n    public String doPatch(\n            @ApiComment(\"用户id\") @PathVariable(\"id\") Long id,\n            @ApiComment(\"用户描述\") String desc) {\n        if (log.isInfoEnabled()) {\n            log.info(\"doPatch, id = {}, desc = {}\", id, desc);\n        }\n        return \"doDelete, id = \" + id + \", desc = \" + desc;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/ShowParamController.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.util.Strings;\nimport org.springframework.web.bind.annotation.*;\nimport org.springframework.web.multipart.MultipartFile;\n\nimport java.io.IOException;\n\n/**\n *\n */\n@Api2Doc(value = \"params\", name = \"演示各种参数生成文档\", order = 200)\n@RestController\n@RequestMapping(value = \"/api2doc/demo/params\")\npublic class ShowParamController {\n\n    @ApiComment(value = \"返回消息文本\", sample = \"requestParam, key = 简单\")\n    @RequestMapping(value = \"/param\", name = \"@RequestParam（默认）\")\n    public String requestParam(\n            @ApiComment(value = \"请求参数\", sample = \"简单\") String key) {\n        String msg = \"requestParam, key = \" + key;\n        return msg;\n    }\n\n    @RequestMapping(value = \"/path/{key}\", name = \"@PathVariable\")\n    public String pathVariable(\n            @ApiComment(value = \"路径变量\", sample = \"我的文档\")\n            @PathVariable String key) {\n        String msg = \"pathVariable, key = \" + key;\n        return msg;\n    }\n\n    /**\n     * TODO:  header 值不能带中文，或特殊字符。\n     */\n    @RequestMapping(value = \"/header\", name = \"@RequestHeader\")\n    public String requestHeader(\n            @ApiComment(value = \"Header 值 h1\", sample = \"abc\")\n            @RequestHeader(\"h1\") String h1,\n            @ApiComment(value = \"Header 值 h2\", sample = \"123\")\n            @RequestHeader(\"h2\") String h2) {\n        String msg = \"RequestHeader, h1 = \" + h1 + \", h2 = \" + h2;\n        return msg;\n    }\n\n    /**\n     * TODO: Cookie 中不能有空格，否则传到服务端是 + 号。\n     *\n     * @param c1\n     * @param c2\n     * @return\n     */\n    @RequestMapping(value = \"/cookie\", method = RequestMethod.GET,\n            name = \"@CookieValue\")\n    public String cookieValue(\n            @ApiComment(value = \"Cookie 值 c1\", sample = \"我的 Cookie 1\")\n            @CookieValue(\"c1\") String c1,\n            @ApiComment(value = \"Cookie 值 c2\", sample = \"我的 Cookie 2\")\n            @CookieValue(\"c2\") String c2) {\n        String msg = \"cookieValue, c1 = \" + c1 + \", c2 = \" + c2;\n        return msg;\n    }\n\n    @RequestMapping(value = \"/part\", method = RequestMethod.POST,\n            name = \"@RequestPart\")\n    public FileInfo[] requestPart(\n            @ApiComment(value = \"文件名称\", sample = \"my-file\")\n                    String name,\n            @ApiComment(value = \"上传文件1\", sample = \"我的文件1.txt\")\n            @RequestPart(\"file1\") MultipartFile file1,\n            @ApiComment(value = \"上传文件2\", sample = \"我的文件2.txt\")\n            @RequestPart(\"file2\") MultipartFile file2) throws IOException {\n        return new FileInfo[]{\n                FileInfo.parse(file1, name + \"-1\"),\n                FileInfo.parse(file2, name + \"-2\")\n        };\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/ShowResultController.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.Api2DocMocker;\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.restpack.RestPackController;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n *\n */\n@Api2Doc(value = \"results\", name = \"演示各种返回类型生成文档\", order = 300)\n@RestPackController  // 对返回数据进行封装。\n@RequestMapping(value = \"/api2doc/demo/results\")\npublic class ShowResultController {\n\n    @ApiComment(value = \"返回简单的字符串\", sample = \"getString: abc\")\n    @GetMapping(value = \"/getString\", name = \"返回简单 String 类型\")\n    public String getString(String value) {\n        String msg = \"getString: \" + value;\n        return msg;\n    }\n\n    @ApiComment(value = \"返回简单的Date类型\", sample = \"1522851993490\")\n    @GetMapping(value = \"/getDate\", name = \"返回简单 Date 类型\")\n    public Date getDate() {\n        return new Date();\n    }\n\n    @ApiComment(value = \"返回数组类型，其元素为简单 String 类型\", sample = \"3\")\n    @GetMapping(value = \"/getStringArray\", name = \"返回简单数组类型\")\n    public List<String> getStrings() {\n        List<String> list = new ArrayList<>();\n        list.add(\"aaa\");\n        list.add(\"bbb\");\n        return list;\n    }\n\n    @ApiComment(value = \"返回自定义JavaBean类型\")\n    @GetMapping(value = \"/getBean\", name = \"返回自定义JavaBean类型\")\n    public User getUser() {\n        return Api2DocMocker.mockBean(User.class);\n    }\n\n    @ApiComment(value = \"返回数组类型，其中的元素为自定义JavaBean类型\")\n    @GetMapping(value = \"/getUsers\", name = \"返回复杂数组类型\")\n    public List<User> getUsers() {\n        return Api2DocMocker.mockList(User.class, 3);\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/User.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.restpack.RestPackIgnore;\n\nimport java.util.Date;\n\npublic class User {\n\n    @ApiComment(value = \"用户id\", sample = \"123\")\n    private Long id;\n\n    @ApiComment(value = \"用户名\", sample = \"terran4j\")\n    private String name;\n\n    @ApiComment(value = \"账号密码\", sample = \"sdfi23skvs\")\n    private String password;\n\n    @ApiComment(value = \"用户所在的组\", sample = \"研发组\")\n    private String group;\n\n    @ApiComment(value = \"用户类型\", sample = \"admin\")\n    private UserType type;\n\n    @ApiComment(value = \"是否已删除\", sample = \"true\")\n    @RestPackIgnore\n    private Boolean deleted;\n\n    @ApiComment(value = \"创建时间\\n也是注册时间。\")\n    private Date createTime;\n\n    public Date getCreateTime() {\n        return createTime;\n    }\n\n    public void setCreateTime(Date createTime) {\n        this.createTime = createTime;\n    }\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }\n\n    public UserType getType() {\n        return type;\n    }\n\n    public void setType(UserType type) {\n        this.type = type;\n    }\n\n    public Boolean getDeleted() {\n        return deleted;\n    }\n\n    public void setDeleted(Boolean deleted) {\n        this.deleted = deleted;\n    }\n\n    public String getGroup() {\n        return group;\n    }\n\n    public void setGroup(String group) {\n        this.group = group;\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/UserController.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.Api2DocMocker;\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.annotations.ApiError;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\n\n@Api2Doc(id = \"demo\", name = \"演示Api2Doc基本用法\", order = 0)\n@ApiComment(seeClass = User.class)\n@RestController\n@RequestMapping(value = \"/api2doc/demo\")\npublic class UserController {\n\n    // http://localhost:8080/api2doc/demo/user/1\n    @Api2Doc(order = 10)\n    @ApiComment(\"根据用户id，查询此用户的信息\")\n    @RequestMapping(name = \"查询单个用户\",\n            value = \"/user/{id}\", method = RequestMethod.GET)\n    public User getUser(@PathVariable(\"id\") Long id) {\n        return Api2DocMocker.mockBean(User.class);\n    }\n\n    // http://localhost:8080/api2doc/demo/users\n    @Api2Doc(order = 20)\n    @ApiComment(\"查询所有用户，按注册时间进行排序。\")\n    @RequestMapping(name = \"查询用户列表\",\n            value = \"/users\", method = RequestMethod.GET)\n    public List<User> getUsers() {\n        return Api2DocMocker.mockList(User.class, 2);\n    }\n\n    // http://localhost:8080/api2doc/demo/group/研发组\n    @Api2Doc(order = 30)\n    @ApiComment(\"根据指定的组名称，查询该组中的所有用户信息。\")\n    @RequestMapping(name = \"查询用户组\",\n            value = \"/group/{group}\", method = RequestMethod.GET)\n    public UserGroup getGroup(@PathVariable(\"group\") String group) {\n        return Api2DocMocker.mockBean(UserGroup.class);\n    }\n\n    // http://localhost:8080/api2doc/demo/user\n    @Api2Doc(order = 40)\n    @ApiComment(\"添加一个新的用户。\")\n    @RequestMapping(name = \"新增用户\",\n            value = \"/user\", method = RequestMethod.POST)\n    public User addUser(String group, String name, UserType type) {\n        return Api2DocMocker.mockBean(User.class);\n    }\n\n    /**\n     * http://localhost:8080/api2doc/demo/user/1\n     */\n    @Api2Doc(order = 50)\n    @ApiComment(\"根据用户id，删除指定的用户\")\n    @ApiError(value = \"user.not.found\", comment = \"此用户不存在！\")\n    @ApiError(value = \"admin.cant.delete\", comment = \"不允许删除管理员用户！\")\n    @RequestMapping(name = \"删除指定用户\",\n            value = \"/user/{id}\", method = RequestMethod.DELETE)\n    public void delete(@PathVariable(\"id\") Long id) {\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/UserController1.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\n@Api2Doc(id = \"demo1\", name = \"演示 seeClass 的用法\", order = 10)\n@ApiComment(seeClass = User.class)\n@RestController\n@RequestMapping(value = \"/api2doc/demo1\")\npublic class UserController1 {\n\n    @ApiComment(\"添加一个新的用户。\\n\" +\n            \"你会发现：即使参数 group, name 上什么也没写，也能显示文档注解\")\n    @RequestMapping(name = \"新增用户\",\n            value = \"/user\", method = RequestMethod.POST)\n    public User addUser(String group, String name,\n                        @ApiComment(\"用户类型\") UserType type) {\n        return null; // TODO:  还未实现。\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/UserController2.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\n\n@Api2Doc(id = \"demo2\", name = \"演示用 order 给文档排序的用法\", order = 20)\n@ApiComment(seeClass = User.class)\n@RestController\n@RequestMapping(value = \"/api2doc/demo2\")\npublic class UserController2 {\n\n    @Api2Doc(order = 10)\n    @ApiComment(\"添加一个新的用户。\")\n    @RequestMapping(name = \"1. 新增用户\",\n            value = \"/user\", method = RequestMethod.POST)\n    public User addUser(\n            @ApiComment(\"用户组名称\") String group,\n            @ApiComment(\"用户名称\") String name,\n            @ApiComment(\"用户类型\") UserType type) {\n        return null; // TODO:  还未实现。\n    }\n\n    @Api2Doc(order = 20)\n    @ApiComment(\"根据用户id，查询此用户的信息\")\n    @RequestMapping(name = \"2. 查询单个用户\",\n            value = \"/user/{id}\", method = RequestMethod.GET)\n    public User getUser(@PathVariable(\"id\") Long id) {\n        return null; // TODO:  还未实现。\n    }\n\n    @Api2Doc(order = 30)\n    @ApiComment(\"查询所有用户，按注册时间进行排序。\")\n    @RequestMapping(name = \"3. 查询用户列表\",\n            value = \"/users\", method = RequestMethod.GET)\n    public List<User> getUsers() {\n        return null; // TODO:  还未实现。\n    }\n\n    @Api2Doc(order = 40)\n    @ApiComment(\"根据指定的组名称，查询该组中的所有用户信息。\")\n    @RequestMapping(name = \"4. 查询用户组\",\n            value = \"/group/{group}\", method = RequestMethod.GET)\n    public UserGroup getGroup(@PathVariable(\"group\") String group) {\n        return null; // TODO:  还未实现。\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/UserController3.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@Api2Doc(id = \"demo3\", name = \"演示添加补充文档的用法\", order = 30)\n@RestController\n@RequestMapping(value = \"/api2doc/demo3\")\npublic class UserController3 {\n\n    @Api2Doc(order = 10)\n    @RequestMapping(name = \"接口1\", value = \"/m1\")\n    public void m1() {\n    }\n\n    @Api2Doc(order = 20)\n    @RequestMapping(name = \"接口2\", value = \"/m2\")\n    public void m2() {\n    }\n\n    @RequestMapping(value = \"/do_something\")\n    public void doSomethingRequiredLogon() {\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/UserGroup.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\n\n@ApiComment(\"一组用户的信息\")\npublic class UserGroup {\n\n    @ApiComment(\"本组用户的组\")\n    private String group;\n\n\t@ApiComment(value = \"本组用户列表\", sample = \"2\")\n\tprivate List<User> users = new ArrayList<>();\n\n\tpublic List<User> getUsers() {\n\t\treturn users;\n\t}\n\n\tpublic void setUsers(List<User> users) {\n\t\tthis.users = users;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/demo/api2doc/UserType.java",
    "content": "package com.terran4j.demo.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\n\npublic enum UserType {\n\n    @ApiComment(\"管理员\")\n    admin,\n\n    @ApiComment(\"普通用户\")\n    user\n}"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/Api2DocCollectorTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class Api2DocCollectorTest {\n\n    private static final Logger log = LoggerFactory.getLogger(Api2DocCollectorTest.class);\n\n    public static class User {\n\n        @ApiComment(value = \"账号id\", sample = \"123\")\n        private Long id;\n\n        public Long getId() {\n            return id;\n        }\n\n        public void setId(Long id) {\n            this.id = id;\n        }\n    }\n\n    @Api2Doc(id = \"user\", name = \"用户相关接口\", order = 0)\n    @RestController\n    @RequestMapping(value = \"/user\")\n    public class UserController {\n\n        @RequestMapping(value = \"/user/{id}\", method = RequestMethod.GET)\n        public User getUser(@PathVariable(\"id\") Long id) {\n            return null;\n        }\n\n        @RequestMapping(value = \"/users\", method = RequestMethod.GET)\n        public List<User> getUsers() {\n            return null;\n        }\n\n        @RequestMapping(value = \"/user/{id}\", method = RequestMethod.POST)\n        public User insert(@PathVariable(\"id\") Long id, //\n                           @ApiComment(value = \"用户名\", sample = \"张三\")\n                           @RequestParam(\"name\") String name) {\n            return null;\n        }\n\n        @RequestMapping(value = \"/user/{id}\", method = RequestMethod.DELETE)\n        public String delete(@PathVariable(\"id\") Long id) {\n            return \"OK\";\n        }\n    }\n\n    @Test\n    public void testToApiFolder() throws Exception {\n        log.info(\"testToApiFolder\");\n        Api2DocCollector collector = new Api2DocCollector();\n        ApiFolderObject folder = collector.toApiFolder(new UserController(), \"userController\");\n        Assert.assertNotNull(folder);\n        Assert.assertEquals(4, folder.getDocs().size());\n\n        // 按 order 及 方法中排序。\n        Assert.assertEquals(\"delete\", folder.getDocs().get(0).getId());\n        Assert.assertEquals(\"getUser\", folder.getDocs().get(1).getId());\n        Assert.assertEquals(\"getUsers\", folder.getDocs().get(2).getId());\n        Assert.assertEquals(\"insert\", folder.getDocs().get(3).getId());\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/Api2DocObjectFactoryTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.impl.Api2DocObjectFactory;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\n\npublic class Api2DocObjectFactoryTest {\n\n    private static final Logger log = LoggerFactory.getLogger(Api2DocObjectFactoryTest.class);\n\n    public enum UserType {\n\n        @ApiComment(\"ROOT用户\")\n        root,\n\n        @ApiComment(\"管理员用户\")\n        admin,\n\n        @ApiComment(\"普通用户\")\n        user,\n\n    }\n\n    public static class User {\n\n        @ApiComment(value = \"用户id\", sample = \"123\")\n        private Long id;\n\n        @ApiComment(value = \"用户名\", sample = \"neo\")\n        private String name;\n\n        @ApiComment(value = \"用户类型\", sample = \"admin\")\n        private UserType type;\n\n        @ApiComment(value = \"是否已删除\", sample = \"true\")\n        private Boolean deleted;\n\n        @ApiComment(value = \"头衔\", sample = \"3\")\n        private String[] titles;\n\n        @ApiComment(value = \"配偶\")\n        private User mate;\n\n        @ApiComment(value = \"子女\", sample = \"2\")\n        private List<User> children;\n\n        public User getMate() {\n            return mate;\n        }\n\n        public void setMate(User mate) {\n            this.mate = mate;\n        }\n\n        public List<User> getChildren() {\n            return children;\n        }\n\n        public void setChildren(List<User> children) {\n            this.children = children;\n        }\n\n        public Long getId() {\n            return id;\n        }\n\n        public void setId(Long id) {\n            this.id = id;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n        public UserType getType() {\n            return type;\n        }\n\n        public void setType(UserType type) {\n            this.type = type;\n        }\n\n        public Boolean getDeleted() {\n            return deleted;\n        }\n\n        public void setDeleted(Boolean deleted) {\n            this.deleted = deleted;\n        }\n\n        public String[] getTitles() {\n            return titles;\n        }\n\n        public void setTitles(String[] titles) {\n            this.titles = titles;\n        }\n    }\n\n    @Test\n    public void testCreateList() throws Throwable {\n        List<User> users = Api2DocObjectFactory.createList(User.class, 3);\n        Assert.assertNotNull(users);\n        Assert.assertEquals(3, users.size());\n        Assert.assertEquals(new Long(123), users.get(0).getId());\n    }\n\n    @Test\n    public void testCreateArray() throws Throwable {\n        User[] users = Api2DocObjectFactory.createArray(User.class, 3);\n        Assert.assertNotNull(users);\n        Assert.assertEquals(3, users.length);\n        Assert.assertEquals(new Long(123), users[0].getId());\n    }\n\n    @Test\n    public void testCreateObject() throws Throwable {\n        User user = Api2DocObjectFactory.createBean(User.class);\n        Assert.assertNotNull(user);\n        Assert.assertEquals(new Long(123), user.getId());\n        Assert.assertEquals(\"neo\", user.getName());\n        Assert.assertEquals(true, user.getDeleted());\n        Assert.assertEquals(UserType.admin, user.getType());\n    }\n\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/Api2DocUtilsTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport com.terran4j.commons.api2doc.impl.Api2DocUtils;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.List;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class Api2DocUtilsTest {\n\n    private static final Logger log = LoggerFactory.getLogger(Api2DocUtilsTest.class);\n\n    @Api2Doc(id = \"user\", name = \"用户相关接口\", order = 0)\n    @RestController\n    @RequestMapping(value = \"/api/v1\")\n    public class Api2DocUtilsTestController {\n\n        @RequestMapping(value = \"/user/{id}\", method = RequestMethod.GET)\n        public Api2DocCollectorTest.User getUser(\n                @ApiComment(value = \"用户id\", sample = \"123\")\n                @PathVariable(\"id\") Long id) {\n            return null;\n        }\n\n        @RequestMapping(value = \"/user2/{id}\", method = RequestMethod.GET)\n        public Api2DocCollectorTest.User getUser2(\n                @ApiComment(value = \"用户id\") // 参数没有指定示例值。\n                @PathVariable(\"id\") Long id,\n                @ApiComment(value = \"年龄\") // 参数没有指定示例值。\n                @RequestParam(\"age\") Integer age) {\n            return null;\n        }\n\n        @RequestMapping(value = \"/group/{groupId}/users\", method = RequestMethod.GET)\n        public List<Api2DocCollectorTest.User> getUsers(\n                @ApiComment(value = \"组id\", sample = \"3\")\n                @PathVariable(\"groupId\") Long groupId,\n                @ApiComment(value = \"所在城市，如： 北京/上海/深圳\", sample = \"北京\")\n                @RequestParam(\"city\") String city,\n                @ApiComment(value = \"年龄\", sample = \"30\")\n                @RequestParam(\"age\") Integer age) {\n            return null;\n        }\n    }\n\n    private String serverURL = \"http://localhost:8080\";\n\n    @Test\n    public void testToURL() throws Exception {\n        log.info(\"testToURL\");\n        Api2DocCollector collector = new Api2DocCollector();\n        ApiFolderObject folder = collector.toApiFolder(new Api2DocUtilsTestController(),\n                \"userController\");\n        ApiDocObject doc = folder.getDoc(\"getUser\");\n        String url = Api2DocUtils.toURL(doc, serverURL);\n        Assert.assertEquals(\"http://localhost:8080/api/v1/user/123\", url);\n\n        doc = folder.getDoc(\"getUser2\");\n        url = Api2DocUtils.toURL(doc, serverURL);\n        Assert.assertEquals(\"http://localhost:8080/api/v1/user2/0\", url);\n\n        doc = folder.getDoc(\"getUsers\");\n        url = Api2DocUtils.toURL(doc, serverURL);\n        Assert.assertEquals(\"http://localhost:8080/api/v1/group/3/users?\" +\n                \"age=30&city=%E5%8C%97%E4%BA%AC\", url);\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ApiCommentUtilsTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport com.terran4j.commons.api2doc.impl.ApiCommentUtils;\nimport com.terran4j.commons.util.Classes;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.lang.reflect.Field;\nimport java.util.List;\n\npublic class ApiCommentUtilsTest {\n\n    public class MyObject {\n\n        @ApiComment(value = \"用户id\", sample = \"123\")\n        private Long id;\n\n        @ApiComment(value = \"用户名\", sample = \"terran4j\")\n        private String name;\n\n        public Long getId() {\n            return id;\n        }\n\n        public void setId(Long id) {\n            this.id = id;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n    }\n\n\n    @Api2Doc\n    @RestController\n    @RequestMapping(value = \"/api/v1/demo\")\n    public static class MyController {\n\n        @RequestMapping(value = \"/user/{userId}\")\n        public void updateUser(\n                @ApiComment(seeClass = MyObject.class, seeField = \"id\")\n                @PathVariable(\"userId\") Long userId,\n                @ApiComment(seeClass = MyObject.class)\n                @RequestParam(\"name\") Integer userName) {\n        }\n    }\n\n    @Test\n    public void testSee() throws Exception {\n        Api2DocCollector collector = new Api2DocCollector();\n        ApiFolderObject folder = collector.toApiFolder(new MyController(),\"myController\");\n        ApiDocObject doc = folder.getDoc(\"updateUser\");\n        List<ApiParamObject> params = doc.getParams();\n        Assert.assertEquals(\"用户id\", params.get(0).getComment().toString());\n        Assert.assertEquals(\"123\", params.get(0).getSample().toString());\n        Assert.assertEquals(\"用户名\", params.get(1).getComment().toString());\n        Assert.assertEquals(\"terran4j\", params.get(1).getSample().toString());\n    }\n\n    public class AnotherObject {\n\n        @ApiComment(seeClass = MyObject.class)\n        private String name;\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n    }\n\n    @Test\n    public void testGetComment() throws Exception {\n        String fieldName = \"name\";\n        Field field = Classes.getField(fieldName, AnotherObject.class);\n        ApiComment apiComment = field.getAnnotation(ApiComment.class);\n        String comment = ApiCommentUtils.getComment(\n                apiComment, null, fieldName);\n        Assert.assertEquals(\"用户名\", comment);\n        String sample = ApiCommentUtils.getSample(\n                apiComment, null, fieldName);\n        Assert.assertEquals(\"terran4j\", sample);\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ApiResultObjectTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDataType;\nimport com.terran4j.commons.api2doc.domain.ApiResultObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocUtils;\nimport com.terran4j.commons.restpack.RestPackIgnore;\nimport com.terran4j.commons.util.value.KeyedList;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.util.ReflectionUtils;\n\nimport java.lang.reflect.Method;\nimport java.util.*;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ApiResultObjectTest {\n\n    private static final Logger log = LoggerFactory.getLogger(ApiResultObjectTest.class);\n\n    public enum UserType {\n\n        @ApiComment(\"启用\")\n        open,\n\n        @ApiComment(\"禁用\")\n        close,\n\n    }\n\n    public static class User {\n\n        @Api2Doc(order = 10)\n        @ApiComment(value = \"账号id\", sample = \"123\")\n        private Long id;\n\n        @Api2Doc(order = 20)\n        @ApiComment(value = \"账号用户名\", sample = \"terran4j\")\n        private String username;\n\n        @Api2Doc(order = 30)\n        @ApiComment(value = \"账号密码\", sample = \"sdfi23skvs\")\n        private String password;\n\n        @Api2Doc(order = 30)\n        @ApiComment(value = \"用户状态\", sample = \"open\")\n        private UserType state;\n\n        @Api2Doc(order = 40)\n        @ApiComment(value = \"是否已删除\", sample = \"true\")\n        @RestPackIgnore\n        private Boolean deleted;\n\n        public Long getId() {\n            return id;\n        }\n\n        public void setId(Long id) {\n            this.id = id;\n        }\n\n        public String getUsername() {\n            return username;\n        }\n\n        public void setUsername(String username) {\n            this.username = username;\n        }\n\n        public String getPassword() {\n            return password;\n        }\n\n        public void setPassword(String password) {\n            this.password = password;\n        }\n\n        public UserType getState() {\n            return state;\n        }\n\n        public void setState(UserType state) {\n            this.state = state;\n        }\n\n        public Boolean getDeleted() {\n            return deleted;\n        }\n\n        public void setDeleted(Boolean deleted) {\n            this.deleted = deleted;\n        }\n    }\n\n\n    public final List<User> getUsers() {\n        return new ArrayList<>();\n    }\n\n    @Test\n    public void testParseResultType() throws Exception {\n        log.info(\"testParseResultType\");\n        Method method = ReflectionUtils.findMethod(getClass(), \"getUsers\");\n        Assert.assertNotNull(method);\n        KeyedList<String, ApiResultObject> totalResults = new KeyedList<>();\n        ApiResultObject object = ApiResultObject.parseResultType(method, totalResults);\n        Assert.assertNotNull(object);\n        Assert.assertEquals(1, totalResults.size());\n        Assert.assertTrue(object == totalResults.get(0));\n        Assert.assertTrue(ApiDataType.ARRAY == object.getDataType());\n        Assert.assertTrue(User.class == object.getSourceType());\n    }\n\n    @ApiComment(\"消息内容\")\n    public final String getMessage() {\n        return \"Got the message!\";\n    }\n\n    @Test\n    public void testParseResultTypeWithSimple() throws Exception {\n        log.info(\"testParseResultTypeWithSimple\");\n        Method method = ReflectionUtils.findMethod(getClass(), \"getMessage\");\n        Assert.assertNotNull(method);\n        KeyedList<String, ApiResultObject> totalResults = new KeyedList<>();\n        ApiResultObject resultObject = ApiResultObject.parseResultType(method, totalResults);\n        Assert.assertNotNull(resultObject);\n        Assert.assertEquals(0, totalResults.size());\n        Assert.assertTrue(ApiDataType.STRING == resultObject.getDataType());\n        Assert.assertTrue(String.class == resultObject.getSourceType());\n    }\n\n    @Test\n    public void testParseResultTypeWithIgnore() throws Exception {\n        log.info(\"testParseResultTypeWithIgnore\");\n        Method method = ReflectionUtils.findMethod(getClass(), \"getUsers\");\n        Assert.assertNotNull(method);\n        KeyedList<String, ApiResultObject> totalResults = new KeyedList<>();\n        ApiResultObject object = ApiResultObject.parseResultType(method, totalResults);\n        Assert.assertNotNull(object);\n        Assert.assertNull(object.getChild(\"deleted\"));\n    }\n\n    public static class DateBean {\n\n        @Api2Doc\n        @ApiComment(value = \"当前日期\")\n        private Date current;\n\n        public Date getCurrent() {\n            return current;\n        }\n\n        public void setCurrent(Date current) {\n            this.current = current;\n        }\n    }\n\n    public DateBean getDateBean() {\n        return new DateBean();\n    }\n\n    @Test\n    public void testParseResultTypeWithDate() throws Exception {\n        log.info(\"testParseResultTypeWithDate\");\n        Method method = ReflectionUtils.findMethod(getClass(), \"getDateBean\");\n        Assert.assertNotNull(method);\n        KeyedList<String, ApiResultObject> totalResults = new KeyedList<>();\n        ApiResultObject object = ApiResultObject.parseResultType(method, totalResults);\n        Assert.assertNotNull(object);\n        ApiResultObject current = object.getChild(\"current\");\n        Assert.assertEquals(\"long\", current.getTypeName());\n    }\n\n\n    public final List<String> getList() {\n        return new ArrayList<String>();\n    }\n\n    public final Set<String> getSet() {\n        return new HashSet<String>();\n    }\n\n    public final String[] getArray() {\n        return new String[0];\n    }\n\n    public final Map<String, Object> getMap() {\n        return new HashMap<String, Object>();\n    }\n\n    @Test\n    public void testGetArrayElementClass() throws Exception {\n        log.info(\"testGetArrayElementClass\");\n        Method method = ReflectionUtils.findMethod(getClass(), \"getList\");\n        Assert.assertNotNull(method);\n        Class<?> clazz = Api2DocUtils.getArrayElementClass(method);\n        Assert.assertEquals(String.class, clazz);\n\n        method = ReflectionUtils.findMethod(getClass(), \"getSet\");\n        Assert.assertNotNull(method);\n        clazz = Api2DocUtils.getArrayElementClass(method);\n        Assert.assertEquals(String.class, clazz);\n\n        method = ReflectionUtils.findMethod(getClass(), \"getArray\");\n        Assert.assertNotNull(method);\n        clazz = Api2DocUtils.getArrayElementClass(method);\n        Assert.assertEquals(String.class, clazz);\n    }\n\n    public static class NotOrderBean {\n\n        private String abc;\n\n        private String abd;\n\n        private String b1;\n\n        private String b2;\n\n        public String getAbc() {\n            return abc;\n        }\n\n        public void setAbc(String abc) {\n            this.abc = abc;\n        }\n\n        public String getAbd() {\n            return abd;\n        }\n\n        public void setAbd(String abd) {\n            this.abd = abd;\n        }\n\n        public String getB1() {\n            return b1;\n        }\n\n        public void setB1(String b1) {\n            this.b1 = b1;\n        }\n\n        public String getB2() {\n            return b2;\n        }\n\n        public void setB2(String b2) {\n            this.b2 = b2;\n        }\n    }\n\n    public NotOrderBean getNotOrderBean() {\n        return null;\n    }\n\n    @Test\n    public void testParseResultTypeWithNotOrder() throws Exception {\n        log.info(\"testParseResultTypeWithNotOrder\");\n        Method method = ReflectionUtils.findMethod(getClass(), \"getNotOrderBean\");\n        Assert.assertNotNull(method);\n        ApiResultObject object = ApiResultObject.parseResultType(method, null);\n        Assert.assertNotNull(object);\n        Assert.assertEquals(4, object.getChildren().size());\n        Assert.assertEquals(\"abc\", object.getChildren().get(0).getId());\n        Assert.assertEquals(\"abd\", object.getChildren().get(1).getId());\n        Assert.assertEquals(\"b1\", object.getChildren().get(2).getId());\n        Assert.assertEquals(\"b2\", object.getChildren().get(3).getId());\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/Application.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.api2doc.config.EnableApi2Doc;\n\n@EnableApi2Doc\n@SpringBootApplication\npublic class Application {\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/BaseApi2DocTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.junit.Assert;\nimport org.junit.runner.RunWith;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\n@RunWith(SpringJUnit4ClassRunner.class)\nabstract public class BaseApi2DocTest {\n\n    protected String serverURL = \"http://localhost:8080\";\n\n    protected Api2DocCollector collector = new Api2DocCollector();\n\n    protected ApiDocObject loadDoc(String methodName) throws BusinessException {\n        ApiFolderObject folder = collector.toApiFolder(\n                this, this.getClass().getSimpleName());\n        ApiDocObject doc = folder.getDoc(methodName);\n        Assert.assertNotNull(doc);\n        return doc;\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/CurlBuilderTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport com.terran4j.commons.api2doc.impl.CurlBuilder;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.web.bind.annotation.*;\n\n@Api2Doc(id = \"curl\")\n@RequestMapping(value = \"/api/v1/curl\")\npublic class CurlBuilderTest extends BaseApi2DocTest {\n\n    @RequestMapping(value = \"/getting\", method = RequestMethod.GET)\n    public void withGet(\n            @ApiComment(sample = \"键1\") String k1,\n            @ApiComment(sample = \"键2\") String k2) {\n    }\n\n    @Test\n    public void testToCurlWithGet() throws Exception {\n        ApiDocObject doc = loadDoc(\"withGet\");\n        String curl = CurlBuilder.toCurl(doc, serverURL);\n        Assert.assertEquals(\n                \"curl -X GET \\\\\\n\" +\n                        \" \\\"http://localhost:8080/api/v1/curl/getting?k1=%E9%94%AE1&k2=%E9%94%AE2\\\"\",\n                curl);\n    }\n\n    @RequestMapping(value = \"/normal/{p1}/abc/{p2}\",\n            method = RequestMethod.POST)\n    public void simple(\n            @PathVariable(\"p1\") Long p1,\n            @PathVariable(\"p2\") String p2,\n            @RequestParam(\"myParam\") String myParam,\n            @RequestHeader(\"myHeader\") String myHeader,\n            @CookieValue(\"myCookie\") String myCookie) {\n    }\n\n    @Test\n    public void testToCurlSimple() throws Exception {\n        ApiDocObject doc = loadDoc(\"simple\");\n        String curl = CurlBuilder.toCurl(doc, serverURL);\n        Assert.assertEquals(\n                \"curl -X POST \\\\\\n\" +\n                        \" -H \\\"myHeader: myHeader\\\" \\\\\\n\" +\n                        \" -b \\\"myCookie=myCookie\\\" \\\\\\n\" +\n                        \" -d \\\"myParam=myParam\\\" \\\\\\n\" +\n                        \" \\\"http://localhost:8080/api/v1/curl/normal/0/abc/p2\\\"\",\n                curl);\n    }\n\n    @RequestMapping(value = \"/comment/{p1}/{p2}\",\n            method = RequestMethod.POST)\n    public void withComment(\n            @ApiComment(value = \"路径参数\", sample = \"123\")\n            @PathVariable(\"p1\") Long p1,\n\n            @ApiComment(value = \"路径参数\", sample = \"abc\")\n            @PathVariable(\"p2\") String p2,\n\n            @ApiComment(value = \"请求参数\", sample = \"k123\")\n            @RequestParam(\"k1\") String k1,\n\n            @ApiComment(value = \"Header参数\", sample = \"false\")\n            @RequestHeader(\"k2\") boolean k2,\n\n            @ApiComment(value = \"Cookie参数\", sample = \"5.86\")\n            @CookieValue(\"k3\") double k3) {\n    }\n\n    @Test\n    public void testToCurlWithComment() throws Exception {\n        ApiDocObject doc = loadDoc(\"withComment\");\n        String curl = CurlBuilder.toCurl(doc, serverURL);\n        Assert.assertEquals(\n                \"curl -X POST \\\\\\n\" +\n                        \" -H \\\"k2: false\\\" \\\\\\n\" +\n                        \" -b \\\"k3=5.86\\\" \\\\\\n\" +\n                        \" -d \\\"k1=k123\\\" \\\\\\n\" +\n                        \" \\\"http://localhost:8080/api/v1/curl/comment/123/abc\\\"\",\n                curl);\n    }\n\n}"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/JavaBeanCodeWriterTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport java.lang.reflect.Method;\nimport java.util.Date;\nimport java.util.Map;\nimport java.util.Set;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.SpringBootTest.WebEnvironment;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.util.ReflectionUtils;\n\nimport com.terran4j.commons.api2doc.codewriter.JavaBeanCodeWriter;\nimport com.terran4j.commons.api2doc.codewriter.MemoryCodeOutput;\nimport com.terran4j.commons.api2doc.domain.ApiResultObject;\nimport com.terran4j.commons.util.value.KeyedList;\n\n@SpringBootTest(classes = { Application.class }, webEnvironment = WebEnvironment.NONE)\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class JavaBeanCodeWriterTest {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(JavaBeanCodeWriterTest.class);\n\n\tpublic class User {\n\n\t\tprivate String name;\n\t\t\n\t\tprivate Date registTime;\n\n\t\tpublic String getName() {\n\t\t\treturn name;\n\t\t}\n\n\t\tpublic void setName(String name) {\n\t\t\tthis.name = name;\n\t\t}\n\n\t\tpublic Date getRegistTime() {\n\t\t\treturn registTime;\n\t\t}\n\n\t\tpublic void setRegistTime(Date registTime) {\n\t\t\tthis.registTime = registTime;\n\t\t}\n\n\t}\n\t\n\tpublic final User getUser() {\n\t\treturn new User();\n\t}\n\t\n\t@Autowired\n\tprivate JavaBeanCodeWriter javaBeanCodeWriter;\n\t\n\t@Test\n\tpublic void testGetModel() throws Exception {\n\t\tlog.info(\"testGetModel\");\n\t\tMethod method = ReflectionUtils.findMethod(getClass(), \"getUser\");\n\t\tKeyedList<String, ApiResultObject> totalResults = new KeyedList<>();\n\t\tApiResultObject user = ApiResultObject.parseResultType(method, totalResults);\n\t\t\n\t\tMap<String, Object> model = javaBeanCodeWriter.getModel(\n\t\t        user, \"User\", null);\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tSet<String> imports = (Set<String>)model.get(\"imports\");\n\t\tlog.info(\"imports: {}\", imports);\n\t\tAssert.assertFalse(imports.contains(Date.class.getName()));\n\t\t\n\t\tMemoryCodeOutput out = new MemoryCodeOutput();\n\t\tjavaBeanCodeWriter.writeCode(user, \"User\", out, null);\n\t\tString code = out.getCode(\"User.java\");\n\t\tlog.info(\"User.java:\\n{}\", code);\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/MyBean.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.demo.api2doc.User;\n\npublic class MyBean {\n\t\n\t@ApiComment(\"是否打开\")\n\tprivate boolean open;\n\t\n\t@ApiComment(\"计数器\")\n\tprivate int counter;\n\t\n\tprivate String message;\n\t\n\tprivate Date createTime;\n\n\tprivate List<User> users = new ArrayList<>();\n\t\n\tprivate MyBean[] children2 = new MyBean[0];\n\t\n\tprivate Set<MyBean> children3 = new HashSet<>();\n\n\tpublic boolean isOpen() {\n\t\treturn open;\n\t}\n\n\tpublic void setOpen(boolean open) {\n\t\tthis.open = open;\n\t}\n\n\tpublic int getCounter() {\n\t\treturn counter;\n\t}\n\n\tpublic void setCounter(int counter) {\n\t\tthis.counter = counter;\n\t}\n\n\tpublic String getMessage() {\n\t\treturn message;\n\t}\n\n\tpublic void setMessage(String message) {\n\t\tthis.message = message;\n\t}\n\n\tpublic Date getCreateTime() {\n\t\treturn createTime;\n\t}\n\n\tpublic void setCreateTime(Date createTime) {\n\t\tthis.createTime = createTime;\n\t}\n\n\tpublic List<User> getUsers() {\n\t\treturn users;\n\t}\n\n\tpublic void setUsers(List<User> users) {\n\t\tthis.users = users;\n\t}\n\n\tpublic MyBean[] getChildren2() {\n\t\treturn children2;\n\t}\n\n\tpublic void setChildren2(MyBean[] children2) {\n\t\tthis.children2 = children2;\n\t}\n\n\tpublic Set<MyBean> getChildren3() {\n\t\treturn children3;\n\t}\n\n\tpublic void setChildren3(Set<MyBean> children3) {\n\t\tthis.children3 = children3;\n\t}\n\n}"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ParseApiCommentOnMethod.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport java.lang.reflect.Method;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.util.ReflectionUtils;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiResultObject;\nimport com.terran4j.commons.util.value.KeyedList;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ParseApiCommentOnMethod {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(ParseApiCommentOnMethod.class);\n\n\tpublic class User {\n\n\t\t@Api2Doc(order = 10)\n\t\t@ApiComment(value = \"账号id\", sample = \"123\")\n\t\tprivate Long id;\n\n\t\tpublic Long getId() {\n\t\t\treturn id;\n\t\t}\n\n\t\tpublic void setId(Long id) {\n\t\t\tthis.id = id;\n\t\t}\n\n\t\t@Api2Doc(order = 20)\n\t\t@ApiComment(value = \"账号用户名\", sample = \"terran4j\")\n\t\tpublic String getUsername() {\n\t\t\treturn String.valueOf(id);\n\t\t}\n\n\t}\n\n\tpublic final User getUser() {\n\t\treturn new User();\n\t}\n\n\t@Test\n\tpublic void testParseApiCommentOnMethod() throws Exception {\n\t\tlog.info(\"testParseApiCommentOnMethod\");\n\t\tMethod method = ReflectionUtils.findMethod(getClass(), \"getUser\");\n\t\tAssert.assertNotNull(method);\n\n\t\tKeyedList<String, ApiResultObject> list = new KeyedList<>();\n\t\tApiResultObject user = ApiResultObject.parseResultType(method, list);\n\t\tAssert.assertNotNull(user);\n\t\tlog.info(\"user: {}\", user);\n\t\tAssert.assertEquals(2, user.getChildren().size());\n\n\t\tApiResultObject userId = user.getChildren().get(0);\n\t\tAssert.assertEquals(\"id\", userId.getId());\n\t\tAssert.assertEquals(\"账号id\", userId.getComment().getValue());\n\t\tAssert.assertEquals(\"123\", userId.getSample().getValue());\n\n\t\tApiResultObject userName = user.getChildren().get(1);\n\t\tAssert.assertEquals(\"username\", userName.getId());\n\t\tAssert.assertEquals(\"账号用户名\", userName.getComment().getValue());\n\t\tAssert.assertEquals(\"terran4j\", userName.getSample().getValue());\n\t}\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ParseApiCommentOnParam.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiParamLocation;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.util.ReflectionUtils;\n\nimport java.lang.reflect.Method;\nimport java.util.List;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ParseApiCommentOnParam {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(ParseApiCommentOnParam.class);\n\n\t@ApiComment\n\tpublic class User {\n\n\t\t@Api2Doc(order = 10)\n\t\t@ApiComment(value = \"账号id\", sample = \"123\")\n\t\tprivate Long id;\n\n\t\t@Api2Doc(order = 20)\n\t\t@ApiComment(value = \"账号用户名\", sample = \"neo\")\n\t\tprivate String name;\n\n\t\tpublic Long getId() {\n\t\t\treturn id;\n\t\t}\n\n\t\tpublic void setId(Long id) {\n\t\t\tthis.id = id;\n\t\t}\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n    }\n\n\tpublic final void setUser(\n\t        @ApiComment(value = \"用户类型\", sample = \"root\") String type,\n            boolean asRoot,\n            User user) {\n\t}\n\n\t@Test\n\tpublic void testParseApiCommentOnParam() throws Exception {\n\t\tlog.info(\"testParseApiCommentOnParam\");\n\t\tMethod method = ReflectionUtils.findMethod(getClass(), \"setUser\",\n                new Class<?>[]{String.class, boolean.class, User.class});\n\t\tAssert.assertNotNull(method);\n\n        Api2DocCollector collector = new Api2DocCollector();\n        List<ApiParamObject> params = collector.toApiParams(method, null);\n\t\tAssert.assertEquals(4, params.size());\n\n\t\tApiParamObject type = params.get(0);\n\t\tAssert.assertEquals(\"type\", type.getId());\n\t\tAssert.assertEquals(\"用户类型\", type.getComment().getValue());\n\t\tAssert.assertEquals(\"root\", type.getSample().getValue());\n\n        ApiParamObject asRoot = params.get(1);\n\t\tAssert.assertEquals(\"asRoot\", asRoot.getId());\n        Assert.assertEquals(ApiParamLocation.RequestParam, asRoot.getLocation());\n\n        ApiParamObject id = params.get(2);\n        Assert.assertEquals(\"id\", id.getId());\n        Assert.assertEquals(\"账号id\", id.getComment().getValue());\n        Assert.assertEquals(\"123\", id.getSample().getValue());\n\n        ApiParamObject name = params.get(3);\n        Assert.assertEquals(\"name\", name.getId());\n        Assert.assertEquals(\"账号用户名\", name.getComment().getValue());\n        Assert.assertEquals(\"neo\", name.getSample().getValue());\n\t}\n\n}"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ParseApiCommentOnSeeClass.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\nimport com.terran4j.commons.api2doc.domain.ApiResultObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ParseApiCommentOnSeeClass {\n\n    private static final Logger log = LoggerFactory.getLogger(ParseApiCommentOnSeeClass.class);\n\n    public class Comments {\n\n        @ApiComment(value = \"用户id\", sample = \"123\")\n        private Long id;\n\n        @ApiComment(value = \"用户名称\", sample = \"neo\")\n        private Long name;\n\n        public Long getId() {\n            return id;\n        }\n\n        public void setId(Long id) {\n            this.id = id;\n        }\n\n        public Long getName() {\n            return name;\n        }\n\n        public void setName(Long name) {\n            this.name = name;\n        }\n    }\n\n    @ApiComment(seeClass = Comments.class)\n    public class User {\n\n        private Long id;\n\n        private Long name;\n\n        public Long getId() {\n            return id;\n        }\n\n        public void setId(Long id) {\n            this.id = id;\n        }\n\n        public Long getName() {\n            return name;\n        }\n\n        public void setName(Long name) {\n            this.name = name;\n        }\n    }\n\n    @Api2Doc\n    @ApiComment(seeClass = Comments.class)\n    @RestController\n    @RequestMapping(value = \"/test/api2doc/seeClass\")\n    public static class MyController {\n\n        @RequestMapping(value = \"/user/{id}\")\n        public User updateUser(\n                @PathVariable(\"id\") Long id,\n                @RequestParam(\"name\") String name) {\n            return null;\n        }\n    }\n\n    @Test\n    public void testParseApiCommentOnSeeClass() throws Exception {\n        log.info(\"testParseApiCommentOnSeeClass\");\n\n        Api2DocCollector collector = new Api2DocCollector();\n        ApiFolderObject folder = collector.toApiFolder(\n                new ParseApiCommentOnSeeClass.MyController(), \"myController\");\n        ApiDocObject doc = folder.getDoc(\"updateUser\");\n\n        List<ApiParamObject> params = doc.getParams();\n        Assert.assertEquals(\"用户id\", params.get(0).getComment().toString());\n        Assert.assertEquals(\"123\", params.get(0).getSample().toString());\n        Assert.assertEquals(\"用户名称\", params.get(1).getComment().toString());\n        Assert.assertEquals(\"neo\", params.get(1).getSample().toString());\n\n        ApiResultObject user = doc.getResults().get(0);\n\n        ApiResultObject userId = user.getChildren().get(0);\n        Assert.assertEquals(\"id\", userId.getId());\n        Assert.assertEquals(\"用户id\", userId.getComment().getValue());\n        Assert.assertEquals(\"123\", userId.getSample().getValue());\n\n        ApiResultObject userName = user.getChildren().get(1);\n        Assert.assertEquals(\"name\", userName.getId());\n        Assert.assertEquals(\"用户名称\", userName.getComment().getValue());\n        Assert.assertEquals(\"neo\", userName.getSample().getValue());\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ParseApiCommentOnSeeClassLoop.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport com.terran4j.commons.api2doc.domain.ApiFolderObject;\nimport com.terran4j.commons.api2doc.domain.ApiParamObject;\nimport com.terran4j.commons.api2doc.domain.ApiResultObject;\nimport com.terran4j.commons.api2doc.impl.Api2DocCollector;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport java.util.List;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ParseApiCommentOnSeeClassLoop {\n\n    private static final Logger log = LoggerFactory.getLogger(\n            ParseApiCommentOnSeeClassLoop.class);\n\n    public class Comments {\n\n        @ApiComment(value = \"用户id\", sample = \"123\")\n        private Long id;\n\n        @ApiComment(value = \"用户名称\", sample = \"neo\")\n        private Long name;\n\n        public Long getId() {\n            return id;\n        }\n\n        public void setId(Long id) {\n            this.id = id;\n        }\n\n        public Long getName() {\n            return name;\n        }\n\n        public void setName(Long name) {\n            this.name = name;\n        }\n    }\n\n    @ApiComment(seeClass = Comments.class)\n    public class User {\n\n        private Long id;\n\n        private Long name;\n\n        public Long getId() {\n            return id;\n        }\n\n        public void setId(Long id) {\n            this.id = id;\n        }\n\n        public Long getName() {\n            return name;\n        }\n\n        public void setName(Long name) {\n            this.name = name;\n        }\n    }\n\n    @Api2Doc\n    @ApiComment(seeClass = User.class)\n    @RestController\n    @RequestMapping(value = \"/test/api2doc/seeClass\")\n    public static class MyController {\n\n        @RequestMapping(value = \"/user/{id}\")\n        public User updateUser(\n                @PathVariable(\"id\") Long id,\n                @RequestParam(\"name\") String name) {\n            return null;\n        }\n    }\n\n    @Test\n    public void testParseApiCommentOnSeeClassLoop() throws Exception {\n        log.info(\"testParseApiCommentOnSeeClass\");\n\n        Api2DocCollector collector = new Api2DocCollector();\n        ApiFolderObject folder = collector.toApiFolder(\n                new ParseApiCommentOnSeeClassLoop.MyController(), \"myController\");\n        ApiDocObject doc = folder.getDoc(\"updateUser\");\n\n        List<ApiParamObject> params = doc.getParams();\n        Assert.assertEquals(\"用户id\", params.get(0).getComment().toString());\n        Assert.assertEquals(\"123\", params.get(0).getSample().toString());\n        Assert.assertEquals(\"用户名称\", params.get(1).getComment().toString());\n        Assert.assertEquals(\"neo\", params.get(1).getSample().toString());\n\n        ApiResultObject user = doc.getResults().get(0);\n\n        ApiResultObject userId = user.getChildren().get(0);\n        Assert.assertEquals(\"id\", userId.getId());\n        Assert.assertEquals(\"用户id\", userId.getComment().getValue());\n        Assert.assertEquals(\"123\", userId.getSample().getValue());\n\n        ApiResultObject userName = user.getChildren().get(1);\n        Assert.assertEquals(\"name\", userName.getId());\n        Assert.assertEquals(\"用户名称\", userName.getComment().getValue());\n        Assert.assertEquals(\"neo\", userName.getSample().getValue());\n    }\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ParseEnumTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport java.lang.reflect.Method;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.ReflectionUtils;\n\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiResultObject;\nimport com.terran4j.commons.util.value.KeyedList;\n\npublic class ParseEnumTest {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(ParseEnumTest.class);\n\n\tpublic enum MyState {\n\n\t\t@ApiComment(\"打开\")\n\t\topen,\n\n\t\t@ApiComment(\"关闭\")\n\t\tclose;\n\t}\n\t\n\tpublic final MyState getState() {\n\t\treturn MyState.open;\n\t}\n\t\n\t@Test\n\tpublic void testGetEnumComment() throws Throwable {\n\t\tString comment = ApiResultObject.getEnumComment(MyState.class);\n\t\tAssert.assertEquals(\"可选值为：\\nopen: 打开; \\nclose: 关闭;\", comment.trim());\n\t}\n\t\n\t@Test\n\tpublic void testParseResultTypeWithEnum() throws Exception {\n\t\tlog.info(\"testParseResultTypeWithEnum\");\n\t\tMethod method = ReflectionUtils.findMethod(getClass(), \"getState\");\n\t\tAssert.assertNotNull(method);\n\n\t\tKeyedList<String, ApiResultObject> list = new KeyedList<>();\n\t\tApiResultObject result = ApiResultObject.parseResultType(method, list);\n\t\tAssert.assertNotNull(result);\n\t\tAssert.assertEquals(\"可选值为：\\nopen: 打开; \\nclose: 关闭;\",\n\t\t\t\tresult.getComment().getValue().trim());\n\t}\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ParseListBeanTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.util.ReflectionUtils;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiResultObject;\nimport com.terran4j.commons.util.value.KeyedList;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ParseListBeanTest {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(ParseListBeanTest.class);\n\n\tpublic class User {\n\n\t\t@Api2Doc(order = 10)\n\t\t@ApiComment(value = \"账号id\", sample = \"123\")\n\t\tprivate Long id;\n\t\t\n\t\t@Api2Doc(order = 20)\n\t\t@ApiComment(value = \"账号用户名\", sample = \"terran4j\")\n\t\tprivate String username;\n\n\t\tpublic Long getId() {\n\t\t\treturn id;\n\t\t}\n\n\t\tpublic void setId(Long id) {\n\t\t\tthis.id = id;\n\t\t}\n\n\t\tpublic String getUsername() {\n\t\t\treturn username;\n\t\t}\n\n\t\tpublic void setUsername(String username) {\n\t\t\tthis.username = username;\n\t\t}\n\t\t\n\t}\n\t\n\tpublic static class ListBean {\n\n\t\t@ApiComment(\"There are many users!\")\n\t\tprivate List<User> users = new ArrayList<>();\n\n\t\tpublic List<User> getUsers() {\n\t\t\treturn users;\n\t\t}\n\n\t\tpublic void setUsers(List<User> users) {\n\t\t\tthis.users = users;\n\t\t}\n\n\t}\n\t\n\tpublic final ListBean getListBean() {\n\t\treturn new ListBean();\n\t}\n\t\n\t@Test\n\tpublic void testGetSourceType() throws Exception {\n\t\tlog.info(\"testGetSourceType\");\n\t\tMethod method = ReflectionUtils.findMethod(getClass(), \"getListBean\");\n\t\tApiResultObject results = ApiResultObject.parseResultType(method,  new KeyedList<>());\n\t\tAssert.assertNotNull(results);\n\t\tlog.info(\"results: {}\", results);\n\t\tAssert.assertTrue(results.getChildren().size() == 1);\n\t\tApiResultObject user = results.getChildren().get(0);\n\t\tAssert.assertEquals(User.class, user.getSourceType());\n\t}\n\n\t@Test\n\tpublic void testParseListBean() throws Exception {\n\t\tlog.info(\"testParseListBean\");\n\t\tMethod method = ReflectionUtils.findMethod(getClass(), \"getListBean\");\n\t\tAssert.assertNotNull(method);\n\n\t\tKeyedList<String, ApiResultObject> list = new KeyedList<>();\n\t\tApiResultObject results = ApiResultObject.parseResultType(method, list);\n\t\tAssert.assertNotNull(results);\n\t\tlog.info(\"results: {}\", results);\n\t\tAssert.assertTrue(list.size() == 2);\n\n\t\tApiResultObject resultTop = list.get(0);\n\t\tlog.info(\"resultTop: {}\", resultTop);\n\t\tAssert.assertTrue(resultTop.getChildren().size() == 1);\n\t\tApiResultObject users = resultTop.getChildren().get(0);\n\t\tAssert.assertEquals(\"users\", users.getId());\n\t\tAssert.assertEquals(\"There are many users!\", users.getComment().getValue());\n\n\t\tApiResultObject resultUser = list.get(1);\n\t\tlog.info(\"resultUser: {}\", resultUser);\n\t\tAssert.assertEquals(\"users\", resultUser.getId());\n\t\tAssert.assertTrue(resultUser.getChildren().size() == 2);\n\t\tApiResultObject userId = resultUser.getChildren().get(0);\n\t\tAssert.assertEquals(\"id\", userId.getId());\n\t\tAssert.assertEquals(\"账号id\", userId.getComment().getValue());\n\t}\n\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ResultSourceTypeTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.util.ReflectionUtils;\n\nimport com.terran4j.demo.api2doc.User;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ResultSourceTypeTest {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(ResultSourceTypeTest.class);\n\t\n\tpublic class UserList {\n\n\t\tprivate List<User> users = new ArrayList<>();\n\n\t\tpublic List<User> getUsers() {\n\t\t\treturn users;\n\t\t}\n\n\t\tpublic void setUsers(List<User> users) {\n\t\t\tthis.users = users;\n\t\t}\n\t\t\n\t}\n\t\n\tpublic UserList getUsers() {\n\t\treturn new UserList();\n\t}\n\n\t@Test\n\tpublic void testGetResultSourceType() throws Exception {\n\t\tlog.info(\"testGetResultSourceType\");\n\t\tMethod method = ReflectionUtils.findMethod(getClass(), \"getUsers\");\n\t\tAssert.assertNotNull(method);\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/java/com/terran4j/test/api2doc/ToMockResultTest.java",
    "content": "package com.terran4j.test.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.api2doc.annotations.ApiComment;\nimport com.terran4j.commons.api2doc.domain.ApiDocObject;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\n\n@Api2Doc(id = \"toMockResult\")\n@RequestMapping(value = \"/api/v1/toMockResult\")\npublic class ToMockResultTest extends BaseApi2DocTest {\n\n    public static class User {\n\n        private Date createTime;\n\n        public Date getCreateTime() {\n            return createTime;\n        }\n\n        public void setCreateTime(Date createTime) {\n            this.createTime = createTime;\n        }\n    }\n\n    @RequestMapping(value = \"/getDate\")\n    public Date getDate(Date date) {\n        return date;\n    }\n\n    @Test\n    public void testDateResultType() throws Exception {\n        ApiDocObject doc = loadDoc(\"getDate\");\n        Object mockResult = doc.toMockResult();\n        Assert.assertNotNull(mockResult);\n        Assert.assertEquals(Date.class, mockResult.getClass());\n    }\n\n    @RequestMapping(value = \"/getUser\")\n    public User getUser() {\n        return new User();\n    }\n\n    @Test\n    public void testDateInResultType() throws Exception {\n        ApiDocObject doc = loadDoc(\"getUser\");\n        Object mockResult = doc.toMockResult();\n        Assert.assertNotNull(mockResult);\n        if (mockResult instanceof User) {\n            User user = (User) mockResult;\n            Assert.assertNotNull(user.createTime);\n        } else {\n            Assert.fail(mockResult.getClass() + \"is NOT \" + User.class);\n        }\n    }\n\n    @ApiComment(sample = \"5\")\n    @RequestMapping(value = \"/getStrings\")\n    public List<String> getStrings() {\n        return new ArrayList<>();\n    }\n\n    @Test\n    public void testSimpleList() throws Exception {\n        ApiDocObject doc = loadDoc(\"getStrings\");\n        Object mockResult = doc.toMockResult();\n        Assert.assertNotNull(mockResult);\n        Assert.assertTrue(mockResult instanceof List);\n    }\n}\n"
  },
  {
    "path": "commons-api2doc/src/test/resources/api2doc/demo3/1-项目简介.md",
    "content": "\n***简介***"
  },
  {
    "path": "commons-api2doc/src/test/resources/api2doc/demo3/11-接口1的补充说明.md",
    "content": "\n***注意事项***"
  },
  {
    "path": "commons-api2doc/src/test/resources/api2doc/demo3/2-技术架构.md",
    "content": "\n***关于本项目***"
  },
  {
    "path": "commons-api2doc/src/test/resources/api2doc/demo3/21-接口2的补充说明.md",
    "content": "\n***附录***"
  },
  {
    "path": "commons-api2doc/src/test/resources/api2doc/demo3/3-应用场景.md",
    "content": "\n***必要条件***"
  },
  {
    "path": "commons-api2doc/src/test/resources/api2doc/welcome.md",
    "content": "\n\n## 目录\n\n* 项目背景\n* Api2Doc 简介\n* 引入 Api2Doc 依赖\n* 启用 Api2Doc 服务\n* 给 Controller 类上添加文档注解\n* 给文档菜单项排序\n* 补充自定义文档\n* 定制欢迎页\n* 定制标题及图标\n* 关闭 Api2Doc 服务 \n\n\n"
  },
  {
    "path": "commons-api2doc/src/test/resources/application.yml",
    "content": "\n# 本地环境\napi2doc:\n  title: Api2Doc示例项目——接口文档\n  icon: https://spring.io/img/homepage/icon-spring-framework.svg\n\nserver:\n  port:  8080\n  ## 这个 url 会在文档页面 URL 示例中被用到。\n  url: http://localhost:8080\n\n---\n# 线上环境\nspring:\n  profiles: online\n\napi2doc:\n  enabled: false"
  },
  {
    "path": "commons-armq/.gitignore",
    "content": "\n*.iml\n*.log\n/target/"
  },
  {
    "path": "commons-armq/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <parent>\n        <artifactId>terran4j-commons-parent</artifactId>\n        <groupId>com.github.terran4j</groupId>\n        <version>1.0.4-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>terran4j-commons-armq</artifactId>\n    <packaging>jar</packaging>\n    <name>terran4j-commons-armq</name>\n    <url>https://github.com/terran4j/commons/tree/master/commons-armq</url>\n    <description>是对阿里云 RocketMQ 的封装。</description>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.gson</groupId>\n            <artifactId>gson</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.aliyun.mq</groupId>\n            <artifactId>mq-http-sdk</artifactId>\n            <version>1.0.2</version>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "commons-armq/src/main/java/com/terran4j/commons/armq/ArmqConfig.java",
    "content": "package com.terran4j.commons.armq;\n\nimport com.aliyun.mq.http.MQClient;\nimport com.terran4j.commons.armq.impl.MessageServiceImpl;\nimport lombok.Data;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Data\n@Configuration\npublic class ArmqConfig {\n\n    @Value(\"${aliyun.accessKeyId}\")\n    private String accessKeyId;\n\n    @Value(\"${aliyun.accessKeySecret}\")\n    private String accessKeySecret;\n\n    @Value(\"${aliyun.rocketMQ.instanceId}\")\n    private String instanceId;\n\n    @Value(\"${aliyun.rocketMQ.endpoint}\")\n    private String endpoint;\n\n    @Bean // destroyMethod = \"close\"\n    public MQClient mqClient() {\n        MQClient mqClient = new MQClient(\n                endpoint, // 设置HTTP接入域名（此处以公共云生产环境为例）\n                accessKeyId, // AccessKey 阿里云身份验证，在阿里云服务器管理控制台创建\n                accessKeySecret // SecretKey 阿里云身份验证，在阿里云服务器管理控制台创建\n        );\n        return mqClient;\n    }\n\n    @Bean\n    public MessageService armqFactory(MQClient mqClient) {\n        return new MessageServiceImpl(instanceId, mqClient);\n    }\n\n}\n"
  },
  {
    "path": "commons-armq/src/main/java/com/terran4j/commons/armq/ConsumerConfig.java",
    "content": "package com.terran4j.commons.armq;\n\nimport lombok.Data;\n\n@Data\npublic class ConsumerConfig {\n\n    /**\n     * 每次拉取多少条消息。\n     */\n    private int batchSize = 1;\n\n    /**\n     * 从 RocketMQ 请求拉取消息时，若没有消息，请求会 hold 住多长时间才返回。\n     */\n    private int pollingSecond = 5;\n\n    /**\n     * 起多少个守护线程，用于不停的拉取消息。\n     */\n    private int threadSize = 1;\n\n    /**\n     * 当拉取不到消息、拉取消息出错、或消费消息出错等异常情况发现时，\n     * 线程睡眠多长时间再试（单位为毫秒）\n     */\n    private long threadSleepTime = 1000;\n\n    /**\n     * 用于消息过滤的 tag 表达式，具体语法请参见阿里云的文档：<br>\n     * https://help.aliyun.com/document_detail/29543.html?spm=a2c4g.11186623.2.16.234e38cbxARe8c#concept-2047069\n     */\n    private String messageTag = null;\n\n}\n"
  },
  {
    "path": "commons-armq/src/main/java/com/terran4j/commons/armq/MessageConsumer.java",
    "content": "package com.terran4j.commons.armq;\n\nimport com.terran4j.commons.util.error.BusinessException;\n\npublic interface MessageConsumer<T> {\n\n    /**\n     * @param key\n     * @param content\n     * @throws BusinessException\n     * @warn 请务必确保消息消费的幂等性（极端情况下存在已消费的消息重复消费的情况）。\n     */\n    void onMessage(String key, T content) throws BusinessException;\n}\n"
  },
  {
    "path": "commons-armq/src/main/java/com/terran4j/commons/armq/MessageEntity.java",
    "content": "package com.terran4j.commons.armq;\n\nimport java.lang.annotation.*;\n\n/**\n * 定义一个 Topic 的实体类：<br>\n * 此 Topic 名称即类的 SimpleName；<br>\n * 此 Topic 中的消息，均为此类的对象 JSON 序列化的文本数据(UTF-8)。\n */\n@Documented\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface MessageEntity {\n\n    /**\n     * @return 对应的 Topic 的名称，若不写即为此类的 SimpleName 。\n     */\n    String topicName() default \"\";\n\n    /**\n     * @return 对应的 groupId，若不写在注册消息消费者时会抛错。\n     */\n    String groupId() default \"\";\n}\n"
  },
  {
    "path": "commons-armq/src/main/java/com/terran4j/commons/armq/MessageService.java",
    "content": "package com.terran4j.commons.armq;\n\nimport com.terran4j.commons.util.error.BusinessException;\n\npublic interface MessageService {\n\n    /**\n     * 发送消息（同步调用）。\n     *\n     * @param content 消息对象（即消息的内容）\n     * @param key     消息的key，可为空，若有请必须保证在本 Topic 是唯一标识。\n     * @param tag     消息的标签，可为空。\n     */\n    void send(Object content, String key, String tag) throws BusinessException;\n\n    /**\n     * 注册消息消费者，会内部创建一个线程池来消费消息。<br>\n     * 本方法属于极简方式，每次取1条消息，没消息时长轮询时间为 10 秒。<br>\n     * 线程池核心线程数为CPU核数，最大线程数是CPU核数两倍，阻塞队列大小为 128 。<br>\n     * 如果你希望根据业务情况调整这些参数，请使用方法：<br>\n     * <code>registConsumer(Class<T> messageEntityClass, ConsumerConfig config)</code>\n     *\n     * @param consumer           消息消息者。\n     * @param messageEntityClass 消息实体类对象。\n     * @param <T>                消息实体类型。\n     */\n    <T> void registConsumer(MessageConsumer<T> consumer,\n                            Class<T> messageEntityClass) throws BusinessException;\n\n    /**\n     * @param messageEntityClass 消息实体类对象。\n     * @param config             对消费者的自定义配置。\n     * @param <T>                消息实体类型。\n     * @see MessageService#registConsumer(MessageConsumer, Class)\n     */\n    <T> void registConsumer(MessageConsumer<T> consumer, Class<T> messageEntityClass,\n                            ConsumerConfig config) throws BusinessException;\n\n    <T> void unregistConsumer(MessageConsumer<T> consumer);\n\n    void unregistAllConsumers();\n}\n"
  },
  {
    "path": "commons-armq/src/main/java/com/terran4j/commons/armq/impl/MessageConsumerTask.java",
    "content": "package com.terran4j.commons.armq.impl;\n\nimport com.aliyun.mq.http.MQConsumer;\nimport com.aliyun.mq.http.common.AckMessageException;\nimport com.aliyun.mq.http.model.Message;\nimport com.terran4j.commons.armq.ConsumerConfig;\nimport com.terran4j.commons.armq.MessageConsumer;\nimport com.terran4j.commons.util.Jsons;\nimport com.terran4j.commons.util.task.LoopExecuteTask;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.http.ConnectionClosedException;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Slf4j\npublic class MessageConsumerTask<T> extends LoopExecuteTask {\n\n    private final MessageConsumerTransfer<T> transfer;\n\n    public MessageConsumerTask(MessageConsumerTransfer<T> transfer) {\n        super(transfer.getConfig().getThreadSleepTime());\n        this.transfer = transfer;\n    }\n\n    @Override\n    protected boolean execute() throws Exception {\n        MQConsumer mqConsumer = transfer.getMqConsumer();\n        MessageConsumer<T> realConsumer = transfer.getConsumer();\n        ConsumerConfig config = transfer.getConfig();\n\n        List<Message> messages;\n        try {\n            // 长轮询消费消息\n            // 长轮询表示如果topic没有消息则请求会在服务端挂住3s，3s内如果有消息可以消费则立即返回\n            messages = mqConsumer.consumeMessage(\n                    config.getBatchSize(), // 一次最多消费3条(最多可设置为16条)\n                    config.getPollingSecond() // 长轮询时间3秒（最多可设置为30秒）\n            );\n        } catch (Throwable e) {\n            // 如果是 InterruptedException ，可能是本消费者被注解了。\n            if (e instanceof InterruptedException) {\n                log.warn(\"MessageConsumerTask was Interrupted, consumer class: {}\",\n                        realConsumer.getClass().getName());\n                return false;\n            }\n            // 如果是连接被关闭了，也没办法搞了，只能撤了。\n            if (e instanceof ConnectionClosedException) {\n                this.stop();\n                log.error(\"MQ Connection was Closed: {}\", e.getMessage());\n                return false;\n            }\n\n            // 其他情况，算成拉取消息出错。\n            log.error(\"拉取消息出错：\" + e.getMessage(), e);\n            try {\n                Thread.sleep(2000);\n            } catch (InterruptedException e1) {\n                log.error(\"InterruptedException: \" + e.getMessage());\n            }\n            return false; // sleep 一段时间再试。\n        }\n        if (messages == null || messages.size() == 0) {\n            // 没有消息，sleep 一段时间再循环。\n            return false;\n        }\n        int totalSize = messages.size();\n        log.info(\"Received messages, size = {}\", totalSize);\n\n        List<String> handles = new ArrayList<>();\n        for (Message message : messages) {\n            String key = message.getMessageKey();\n            String body = message.getMessageBodyString();\n            log.info(\"Received message, key = {}, body = {}\", key, body);\n            T content = Jsons.toObject(body, transfer.getMessageEntityClass());\n            try {\n                realConsumer.onMessage(key, content);\n                handles.add(message.getReceiptHandle());\n            } catch (Throwable e) {\n                log.error(\"consume message failed: \" + e.getMessage(), e);\n            }\n        }\n\n        int successSize = handles.size();\n        if (successSize == 0) {\n            // 全部消息都消费失败了，过一段时间再试。\n            return false;\n        } else {\n            try {\n                mqConsumer.ackMessage(handles);\n                log.info(\"Ack message success.\");\n            } catch (AckMessageException e) {\n                // 某些消息的句柄可能超时了会导致确认不成功\n                AckMessageException errors = (AckMessageException) e;\n                log.error(\"Ack message fail, requestId is: {}\", errors.getRequestId());\n                if (errors.getErrorMessages() != null) {\n                    for (String errorHandle : errors.getErrorMessages().keySet()) {\n                        log.error(\"Handle: {}, ErrorCode: {}, ErrorMsg: {}\",\n                                errorHandle, errors.getErrorMessages().get(errorHandle).getErrorCode(),\n                                errors.getErrorMessages().get(errorHandle).getErrorMessage());\n                    }\n                }\n            } catch (Throwable e) {\n                log.error(\"Ack message fail: \" + e.getMessage());\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "commons-armq/src/main/java/com/terran4j/commons/armq/impl/MessageConsumerTransfer.java",
    "content": "package com.terran4j.commons.armq.impl;\n\nimport com.aliyun.mq.http.MQConsumer;\nimport com.terran4j.commons.armq.ConsumerConfig;\nimport com.terran4j.commons.armq.MessageConsumer;\nimport com.terran4j.commons.util.error.BusinessException;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * 起一组线程，以死循环的方式来拉取消息并执行。\n *\n * @param <T> 消息实例类型。\n */\n@Slf4j\npublic class MessageConsumerTransfer<T> {\n\n    private final MQConsumer mqConsumer;\n\n    private final MessageConsumer<T> consumer;\n\n    private final Class<T> messageEntityClass;\n\n    private final ConsumerConfig config;\n\n    private final MessageConsumerTask[] tasks;\n\n    private final Thread[] threads;\n\n    public MessageConsumer<T> getConsumer() {\n        return consumer;\n    }\n\n    public Class<T> getMessageEntityClass() {\n        return messageEntityClass;\n    }\n\n    public ConsumerConfig getConfig() {\n        return config;\n    }\n\n    public MQConsumer getMqConsumer() {\n        return mqConsumer;\n    }\n\n    public MessageConsumerTransfer(MQConsumer mqConsumer,\n                                   MessageConsumer<T> consumer,\n                                   Class<T> messageEntityClass,\n                                   ConsumerConfig config) {\n        this.mqConsumer = mqConsumer;\n        this.consumer = consumer;\n        this.messageEntityClass = messageEntityClass;\n        this.config = config;\n\n        int threadSize = config.getThreadSize();\n        if (threadSize <= 0) {\n            threadSize = 1;\n        }\n        if (threadSize > 100) {\n            threadSize = 100; // 最大不能超过 100 个线程。\n        }\n        tasks = new MessageConsumerTask[threadSize];\n        threads = new Thread[threadSize];\n    }\n\n    public void stop() {\n        for (int i = 0; i < threads.length; i++) {\n            // 如果线程还活着，则标记线程中断。\n            Thread thread = threads[i];\n            if (thread != null && thread.isAlive()) {\n                thread.interrupt();\n            }\n\n            MessageConsumerTask<T> task = tasks[i];\n            for (int j = 0; j < 10; j++) {\n                if (task.isRunning()) {\n                    try {\n                        Thread.sleep(config.getPollingSecond() * 100);\n                    } catch (InterruptedException e) {\n                        log.error(\"stop consumer occur InterruptedException: {}\", e.getMessage());\n                    }\n                }\n            }\n            threads[i] = null;\n            tasks[i] = null;\n        }\n    }\n\n    public void start() throws BusinessException {\n        for (int i = 0; i < threads.length; i++) {\n\n            // 如果线程还活着，则标记线程中断。\n            Thread thread = threads[i];\n            if (thread != null && thread.isAlive()) {\n                thread.interrupt();\n            }\n\n            // 创建新的轮循任务及对应的线程对象。\n            MessageConsumerTask task = new MessageConsumerTask(this);\n            thread = new Thread(task);\n            thread.setDaemon(true);\n            String threadName = \"ARMQ-Consumer-\"\n                    + MessageServiceImpl.getTopicName(messageEntityClass)\n                    + \"-\" + i;\n            thread.setName(threadName);\n            thread.start();\n\n            // 保持在数组中。\n            tasks[i] = task;\n            threads[i] = thread;\n        }\n    }\n\n}\n"
  },
  {
    "path": "commons-armq/src/main/java/com/terran4j/commons/armq/impl/MessageServiceImpl.java",
    "content": "package com.terran4j.commons.armq.impl;\n\nimport com.aliyun.mq.http.MQClient;\nimport com.aliyun.mq.http.MQConsumer;\nimport com.aliyun.mq.http.MQProducer;\nimport com.aliyun.mq.http.model.TopicMessage;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.terran4j.commons.armq.ConsumerConfig;\nimport com.terran4j.commons.armq.MessageConsumer;\nimport com.terran4j.commons.armq.MessageEntity;\nimport com.terran4j.commons.armq.MessageService;\nimport com.terran4j.commons.util.Jsons;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.stereotype.Component;\n\nimport java.io.UnsupportedEncodingException;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n@Slf4j\n@Component\npublic class MessageServiceImpl implements MessageService {\n\n    private final Map<Class<?>, MQProducer> producers = new ConcurrentHashMap<>();\n\n    private final Map<MessageConsumer<?>, MessageConsumerTransfer<?>>\n            consumers = new ConcurrentHashMap<>();\n\n    private final String instanceId;\n\n    private final MQClient mqClient;\n\n    public MessageServiceImpl(String instanceId, MQClient mqClient) {\n        this.instanceId = instanceId;\n        this.mqClient = mqClient;\n    }\n\n    private MQProducer getOrCreateProducer(Class<?> messageEntityClass) throws BusinessException {\n        MQProducer producer = producers.get(messageEntityClass);\n        if (producer != null) {\n            return producer;\n        }\n\n        synchronized (messageEntityClass) {\n            producer = producers.get(messageEntityClass);\n            if (producer != null) {\n                return producer;\n            }\n\n            MessageEntity messageEntity = getMessageEntity(messageEntityClass);\n            String topicName = getTopicName(messageEntity, messageEntityClass);\n            producer = mqClient.getProducer(instanceId, topicName);\n            producers.put(messageEntityClass, producer);\n            return producer;\n        }\n    }\n\n    private static MessageEntity getMessageEntity(Class<?> messageEntityClass) throws BusinessException {\n        if (messageEntityClass == null) {\n            throw new BusinessException(ErrorCodes.NULL_PARAM)\n                    .setMessage(\"messageEntityClass is null.\");\n        }\n\n        MessageEntity messageEntity = messageEntityClass.getAnnotation(MessageEntity.class);\n        if (messageEntity == null) {\n            String msg = String.format(\"消息实体类上没有 @%s 注解： %s\",\n                    MessageEntity.class.getSimpleName(), messageEntityClass.getName());\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n                    .setMessage(msg);\n        }\n        return messageEntity;\n    }\n\n    private static String getTopicName(MessageEntity messageEntity, Class<?> messageEntityClass) {\n        String topicName = messageEntity.topicName();\n        if (StringUtils.isBlank(topicName)) {\n            topicName = messageEntityClass.getSimpleName() + \"Topic\";\n        }\n        return topicName;\n    }\n\n    private static String getGroupId(MessageEntity messageEntity, Class<?> messageEntityClass) {\n        String groupId = messageEntity.groupId();\n        if (StringUtils.isBlank(groupId)) {\n            groupId = \"GID_\" + messageEntityClass.getSimpleName();\n//            throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n//                    .setMessage(\"注册消息消费者时，消息实体类【${messageEntityClass}】的注解上没有指定 groupId.\")\n//                    .put(\"messageEntityClass\", messageEntityClass.getName());\n        }\n        return groupId;\n    }\n\n    public static final <T> String getTopicName(Class<T> messageEntityClass) throws BusinessException {\n        MessageEntity messageEntity = getMessageEntity(messageEntityClass);\n        String topicName = getTopicName(messageEntity, messageEntityClass);\n        return topicName;\n    }\n\n    @Override\n    public void send(Object contentObject, String key, String tag) throws BusinessException {\n        if (contentObject == null) {\n            throw new NullPointerException(\"message is null.\");\n        }\n        Class<?> clazz = contentObject.getClass();\n        MQProducer producer = getOrCreateProducer(clazz);\n\n        String contentText;\n        try {\n            contentText = Jsons.toJsonText(contentObject);\n        } catch (JsonProcessingException e) {\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, e)\n                    .setMessage(\"Java对象序列化成JSON串出错：${cause}\")\n                    .put(\"message\", contentObject.toString()).put(\"cause\", e.getMessage());\n        }\n        if (contentText == null) {\n            throw new BusinessException(ErrorCodes.NULL_PARAM)\n                    .setMessage(\"messageText is null.\");\n        }\n\n        byte[] contentData;\n        try {\n            contentData = contentText.getBytes(\"UTF-8\"); // 消息内容\n        } catch (UnsupportedEncodingException e) {\n            // 理论上不会发生这种情况。\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, e)\n                    .setMessage(\"字符串不是 UTF-8 编码：${cause}\")\n                    .put(\"cause\", e.getMessage()).put(\"messageText\", contentText);\n        }\n        if (contentData == null) {\n            throw new BusinessException(ErrorCodes.NULL_PARAM)\n                    .setMessage(\"messageContent is null.\");\n        }\n\n        TopicMessage msg = new TopicMessage(contentData, tag);\n        msg.setMessageKey(key);\n        producer.publishMessage(msg);\n        log.info(\"publishMessage, key = {}, content = {}\", contentText);\n    }\n\n    @Override\n    public <T> void registConsumer(MessageConsumer<T> consumer,\n                                   Class<T> messageEntityClass) throws BusinessException {\n        registConsumer(consumer, messageEntityClass, new ConsumerConfig());\n    }\n\n    @Override\n    public <T> void registConsumer(MessageConsumer<T> consumer, Class<T> messageEntityClass,\n                                   ConsumerConfig config) throws BusinessException {\n        if (consumer == null) {\n            throw new NullPointerException(\"consumer is null.\");\n        }\n        // 如果不指定，则使用默认配置。\n        if (config == null) {\n            config = new ConsumerConfig();\n        }\n\n        if (consumers.containsKey(consumer)) {\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n                    .setMessage(\"对同一个消费者对象，只能注册一次：${consumer}\")\n                    .put(\"consumer\", consumer);\n        }\n        synchronized (consumer) {\n            if (consumers.containsKey(consumer)) {\n                throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n                        .setMessage(\"对同一个消费者对象，只能注册一次：${consumer}\")\n                        .put(\"consumer\", consumer);\n            }\n\n            // 创建一个消息中转器，从 MQ 中取消息然后交给业务 consumer 对象来处理。\n            MessageEntity messageEntity = getMessageEntity(messageEntityClass);\n            String topicName = getTopicName(messageEntity, messageEntityClass);\n            String groupId = getGroupId(messageEntity, messageEntityClass);\n            MQConsumer mqConsumer = mqClient.getConsumer(instanceId,\n                    topicName, groupId, config.getMessageTag());\n            MessageConsumerTransfer transfer = new MessageConsumerTransfer(\n                    mqConsumer, consumer, messageEntityClass, config);\n            transfer.start();\n            consumers.put(consumer, transfer);\n        }\n    }\n\n    @Override\n    public <T> void unregistConsumer(MessageConsumer<T> consumer) {\n        MessageConsumerTransfer<?> transfer = consumers.get(consumer);\n        transfer.stop();\n        consumers.remove(consumer);\n    }\n\n    @Override\n    public void unregistAllConsumers() {\n        Iterator<MessageConsumer<?>> it = consumers.keySet().iterator();\n        while (it.hasNext()) {\n            MessageConsumer<?> consumer = it.next();\n            unregistConsumer(consumer);\n        }\n\n    }\n}\n"
  },
  {
    "path": "commons-armq/src/test/java/com/terran4j/commons/test/armq/ArmqTestApp.java",
    "content": "package com.terran4j.commons.test.armq;\n\nimport com.terran4j.commons.armq.ArmqConfig;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Import;\n\n@Import(ArmqConfig.class)\n@SpringBootApplication\npublic class ArmqTestApp {\n}\n"
  },
  {
    "path": "commons-armq/src/test/java/com/terran4j/commons/test/armq/Normal.java",
    "content": "package com.terran4j.commons.test.armq;\n\nimport com.terran4j.commons.armq.MessageEntity;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\n\nimport java.util.List;\nimport java.util.Map;\n\n@EqualsAndHashCode\n@Data\n@MessageEntity\npublic class Normal {\n\n    private Long type;\n\n    private String msg;\n\n    private List<Map<String, String>> records;\n}\n"
  },
  {
    "path": "commons-armq/src/test/java/com/terran4j/commons/test/armq/NormalTopicTest.java",
    "content": "package com.terran4j.commons.test.armq;\n\nimport com.terran4j.commons.armq.ArmqConfig;\nimport com.terran4j.commons.armq.MessageConsumer;\nimport com.terran4j.commons.armq.MessageService;\nimport com.terran4j.commons.util.error.BusinessException;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.After;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.CountDownLatch;\n\n@Slf4j\n@SpringBootTest(classes = {ArmqConfig.class})\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class NormalTopicTest implements MessageConsumer<Normal> {\n\n    @Autowired\n    private MessageService messageService;\n\n    private Map<String, Normal> consumedMessages = new ConcurrentHashMap<>();\n\n    private CountDownLatch latch = new CountDownLatch(1);\n\n    @Before\n    public void setUp() throws BusinessException {\n        consumedMessages.clear();\n        latch = new CountDownLatch(1);\n        messageService.registConsumer(this, Normal.class);\n    }\n\n    @After\n    public void tearDown() {\n        messageService.unregistConsumer(this);\n    }\n\n    @Override\n    public void onMessage(String key, Normal content) {\n        consumedMessages.put(key, content);\n        latch.countDown();\n    }\n\n    @Test\n    public void testNormalTopic() throws Exception {\n        log.info(\"testNormalTopic\");\n\n        String key = \"normal-5\";\n        Normal msg = new Normal();\n        msg.setType(5L);\n        msg.setMsg(\"normal msg!\");\n        List<Map<String, String>> records = new ArrayList<>();\n        Map<String, String> record1 = new HashMap<>();\n        record1.put(\"id\", \"1\");\n        records.add(record1);\n        Map<String, String> record2 = new HashMap<>();\n        record2.put(\"id\", \"2\");\n        records.add(record2);\n        msg.setRecords(records);\n\n        long t0 = System.currentTimeMillis();\n        messageService.send(msg, key, null);\n        latch.await();\n        Normal consumedMsg = consumedMessages.get(key);\n        Assert.assertEquals(msg, consumedMsg);\n        long t1 = System.currentTimeMillis();\n        log.info(\"send and receive msg ,spend: {}\", (t1 - t0));\n    }\n\n}\n"
  },
  {
    "path": "commons-armq/src/test/java/com/terran4j/commons/test/armq/Producer.java",
    "content": "package com.terran4j.commons.test.armq;\n\nimport com.aliyun.mq.http.MQClient;\nimport com.aliyun.mq.http.MQProducer;\nimport com.aliyun.mq.http.model.TopicMessage;\n\nimport java.util.Date;\n\npublic class Producer {\n\n    public static void main(String[] args) {\n        MQClient mqClient = new MQClient(\n                // 设置HTTP接入域名（此处以公共云生产环境为例）\n                \"http://1709590499000097.mqrest.cn-beijing.aliyuncs.com\",\n                // AccessKey 阿里云身份验证，在阿里云服务器管理控制台创建\n                \"LTAI4FtZipmeSr6Yk9gXCgWU\",\n                // SecretKey 阿里云身份验证，在阿里云服务器管理控制台创建\n                \"vDpl7t5eo2YyXFfXRT4eushcfBjqGZ\"\n        );\n\n        // 所属的 Topic\n        final String topic = \"NormalTopicTest\";\n        // Topic所属实例ID，默认实例为空\n        final String instanceId = \"\";\n\n        // 获取Topic的生产者\n        MQProducer producer;\n        if (instanceId != null && instanceId != \"\") {\n            producer = mqClient.getProducer(instanceId, topic);\n        } else {\n            producer = mqClient.getProducer(topic);\n        }\n\n        try {\n            // 循环发送4条消息\n            for (int i = 0; i < 4; i++) {\n                TopicMessage pubMsg;\n                if (i % 2 == 0) {\n                    // 普通消息\n                    pubMsg = new TopicMessage(\n                            // 消息内容\n                            \"hello mq!\".getBytes(),\n                            // 消息标签\n                            \"A\"\n                    );\n                    // 设置属性\n                    pubMsg.getProperties().put(\"a\", String.valueOf(i));\n                    // 设置KEY\n                    pubMsg.setMessageKey(\"MessageKey\");\n                } else {\n                    pubMsg = new TopicMessage(\n                            // 消息内容\n                            \"hello mq!\".getBytes(),\n                            // 消息标签\n                            \"A\"\n                    );\n                    // 设置属性\n                    pubMsg.getProperties().put(\"a\", String.valueOf(i));\n                    // 定时消息, 定时时间为10s后\n                    pubMsg.setStartDeliverTime(System.currentTimeMillis() + 10 * 1000);\n                }\n                // 同步发送消息，只要不抛异常就是成功\n                TopicMessage pubResultMsg = producer.publishMessage(pubMsg);\n\n                // 同步发送消息，只要不抛异常就是成功\n                System.out.println(new Date() + \" Send mq message success. \" +\n                        \"Topic is:\" + topic + \", msgId is: \" + pubResultMsg.getMessageId() + \", \" +\n                        \"bodyMD5 is: \" + pubResultMsg.getMessageBodyMD5());\n            }\n        } catch (Throwable e) {\n            // 消息发送失败，需要进行重试处理，可重新发送这条消息或持久化这条数据进行补偿处理\n            System.out.println(new Date() + \" Send mq message failed. Topic is:\" + topic);\n            e.printStackTrace();\n        }\n\n        mqClient.close();\n    }\n}\n"
  },
  {
    "path": "commons-armq/src/test/java/com/terran4j/commons/test/armq/SendTest.java",
    "content": "//package com.terran4j.commons.test.mmq;\n//\n//import com.aliyun.openservices.ons.api.*;\n//\n//import java.io.UnsupportedEncodingException;\n//import java.util.Date;\n//import java.util.Properties;\n//\n//public class SendTest {\n//\n////    AccessKeyID:   LTAI4FtZipmeSr6Yk9gXCgWU\n////    AccessKeySecret:   vDpl7t5eo2YyXFfXRT4eushcfBjqGZ\n//\n//    public static void main(String[] args) throws UnsupportedEncodingException {\n//        Properties properties = new Properties();\n//        // AccessKey 阿里云身份验证，在阿里云用户信息管理控制台获取\n//        properties.put(PropertyKeyConst.AccessKey, \"LTAI4FtZipmeSr6Yk9gXCgWU\");\n//        // SecretKey 阿里云身份验证，在阿里云用户信息管理控制台获取\n//        properties.put(PropertyKeyConst.SecretKey, \"vDpl7t5eo2YyXFfXRT4eushcfBjqGZ\");\n//        //设置发送超时时间，单位毫秒\n//        properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis, \"3000\");\n//        // 设置 TCP 接入域名，进入控制台的实例详情页面的获取接入点信息区域查看\n//        properties.put(PropertyKeyConst.NAMESRV_ADDR,\n//                \"http://1709590499000097.mqrest.cn-beijing.aliyuncs.com\");\n////        properties.put(PropertyKeyConst.GROUP_ID, \"GID-yike-test\");\n//\n//        Producer producer = ONSFactory.createProducer(properties);\n//        // 在发送消息前，必须调用 start 方法来启动 Producer，只需调用一次即可\n//        producer.start();\n//\n//        //循环发送消息\n//        for (int i = 0; i < 3; i++) {\n//            Message msg = new Message( //\n//                    // Message 所属的 Topic\n//                    \"TopicTestMQ\",\n//                    // Message Tag 可理解为 Gmail 中的标签，对消息进行再归类，方便 Consumer 指定过滤条件在消息队列 RocketMQ 版的服务器过滤\n//                    \"TagA\",\n//                    // Message Body 可以是任何二进制形式的数据，消息队列 RocketMQ 版不做任何干预\n//                    // 需要 Producer 与 Consumer 协商好一致的序列化和反序列化方式\n//                    \"Hello MQ\".getBytes(\"UTF-8\"));\n//            // 设置代表消息的业务关键属性，请尽可能全局唯一。\n//            // 以方便您在无法正常收到消息情况下，可通过阿里云服务器管理控制台查询消息并补发\n//            // 注意：不设置也不会影响消息正常收发\n//            msg.setKey(\"ORDERID-\" + i);\n//\n//            try {\n//                SendResult sendResult = producer.send(msg);\n//                // 同步发送消息，只要不抛异常就是成功\n//                if (sendResult != null) {\n//                    System.out.println(new Date() + \" Send mq message success. \" +\n//                            \"Topic is:\" + msg.getTopic() + \" msgId is: \"\n//                            + sendResult.getMessageId());\n//                }\n//            } catch (Exception e) {\n//                // 消息发送失败，需要进行重试处理，可重新发送这条消息或持久化这条数据进行补偿处理\n//                System.out.println(new Date() + \" Send mq message failed. Topic is:\" + msg.getTopic());\n//                e.printStackTrace();\n//            }\n//        }\n//\n//        // 在应用退出前，销毁 Producer 对象\n//        // 注意：如果不销毁也没有问题\n//        producer.shutdown();\n//    }\n//}\n"
  },
  {
    "path": "commons-armq/src/test/resources/.gitignore",
    "content": "\napplication.yml\n"
  },
  {
    "path": "commons-dsql/.gitignore",
    "content": "/target/\n/.classpath\n/.project\n/.settings/\n*.iml"
  },
  {
    "path": "commons-dsql/README.md",
    "content": "\n\n## 目录\n\n* 项目背景\n* DSQL 简介\n* 示例程序介绍\n* 引入 DSQL 依赖\n* 定义实体类\n* 编写 JpaRepository\n* 编写 DsqlRepository\n* 省略 @Param 注解\n* 返回结果的类型映射\n* 使用 @Modifying 进行增删改操作\n* 总结\n\n\n## 项目背景\n\n目前在编写持久层代码时，有两个主流的框架可选，一个是 JPA（实现是Hibernate），另一个是 MyBatis，\n它们各有优缺点，适用于不同的应用场景。\n\nJPA 的优点是：\n* 会根据实体类自动生成数据库结构，\n    在开发阶段，改改代码中类的属性，重启就自动改了数据库表结构，非常的方便。\n* 一些简单的增删改查实现，JPA 可以按约定的方法定义 Repository 接口方法，\n    如： findByNameAndAge(String name, int age) 意思就是根据 name 和 age 两个字段查询，\n    连 SQL 都不用写了，也非常的方便。\n\n当然，JPA 的缺点也很明显：\n* 纯 ORM 的方式，学习成本比较高，\n    JPA 可以用注解的方式，建立实体间的各种关联，如：\n    one-to-one、one-to-many、many-to-one、many-to-many\n    但用好这个比较难，如果不够精通，很容易搞出问题又难以解决。 \n* 对复杂 SQL 编写支持不够好，\n    虽然 JPA 提供了 JpaSpecificationExecutor 这种更灵活的 API，\n    但这种把 SQL 逻辑写到代码的方式，实在是不敢恭维，\n    万一开发人员要请 DBA 帮忙对 SQL 进行优化咋办，让 DBA 先学一遍 Java 和 JPA ？  \n    有人或许会问：“@Query(nativeQuery) 不是可以嵌入原生 SQL 么？”，\n    问题是 @Query 注解中的 SQL 不能动态啊，如果要根据参数不同，执行的 SQL 也不同咋办？\n\n所以 JPA 更适合快速迭代的中小型项目，\n开发时对象的变更非常频繁，又没有大量的复杂 SQL 需求。\n\n我们再来看看 MyBatis，MyBatis 是一个能够灵活编写 SQL，\n并将 SQL 的入参和查询结果映射实体对象的一个持久层框架。\n因此 JPA 的缺点正是它的优点，只要您熟练掌握 SQL 就可以用好 MyBatis\n（而大多数开发人员，是具备这一技能要求的）。\n在性能优化的时候，使用 MyBatis 可以较为方便的调整 SQL 语句，\n甚至可以交给 DBA 或懂 SQL 的业务人员直接对 SQL 进行调整。\n\n综合以上分析来看，最佳的实践方案是： \n使用 JPA 实现实体类到数据库的自动维护，以及简单增删查改逻辑的实现，\n而对于复杂 SQL 操作需求，还是使用类似于 MyBatis 一样，\n手工编写 SQL 并将结果自动映射到实体对象的方式比较好。\n\n因此，DSQL 项目便应运而生了，它对 JPA 进行了扩展，\n主要解决 JPA 不擅长编写复杂 SQL 的问题，\n让您的项目可以使用 JPA 和 MyBatis 两者的优点，可以说是鱼与熊掌兼得。\n\n\n## DSQL 简介\n\nDSQL 允许我们更简单的方法编写原生的、动态的SQL，\n下面我们用一段代码来展示一下。\n\n比如，用 DSQL 定义一个 DAO 方法：\n\n```java\npublic interface AddressDistanceDAO extends DsqlRepository<AddressDistance> {\n\n    @Query(\"address-list\")\n    List<AddressDistance> getAll(AddressQuery args);\n}\n```\n\n与 JPA 一样，这个接口不需要实现类，@Query(\"address-list\") 的意思是：\n在调用这个方法时，会使用名为 address-list.sql.ftl 文件中的 SQL  执行查询，\n查询结果自动映射到返回类型 List<AddressDistance>上。 \n\n文件 address-list.sql.ftl 写在这个接口类所在的 package 中，内容如：\n\n```ftl\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{args.lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{args.lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{args.lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nwhere 1 = 1\n<#if args.name ??>\n    and name like @{args.name}\n</#if>\nORDER BY distance <#if args.nearFirst>ASC<#else>DESC</#if>\n```\n\n本质上是一条 SQL 语句，但可以用 FreeMarker 的语法编写控制逻辑，\n还可以用 @{...} 来使用入参。\n除此之外，不需要额外编写 XML 指定映射关系，\n是不是比 JPA 及 MyBatis 都简单呢？\n\n\n\n## 示例程序介绍\n\n下面我们就要正式学习 DSQL 了，\n本教程会用一个“地理位置查询”的示例程序来讲述 DSQL 的用法。\n\n“地理位置查询”的功能是这样的：\n1. 有一个表记录所有的地理位置信息，包括位置的名称、位置的经纬度，等字段。\n2. 根据指定的位置，查询附近的位置列表，并返回位置之间的距离。\n3. 可以根据位置名称模糊匹配。\n4. 可根据位置远近进行排序条件，可指定是由近及远排序，还是由远及近排序。\n\n这个示例程序的代码，已经放在 src/test/java 目录中 com.terran4j.demo.dsql 包里面，\n本地装好数据库的情况下，是可以运行的，大家在学习过程中可以参考。\n\n\n## 引入 DSQL 依赖\n\n首先，您就可以在您的项目的 pom.xml 文件中，引用 dsql 的 jar 包了，如下所示：\n\n```xml\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-dsql</artifactId>\n\t\t\t<version>${dsql.version}</version>\n\t\t</dependency>\n```\n\n如果是 gradle，请在 build.gradle 中添加依赖，如下所示：\n\n```groovy\ncompile \"com.github.terran4j:terran4j-commons-dsql:${dsql.version}\"\n```\n\n${dsql.version} **最新稳定版，请参考 [这里](https://github.com/terran4j/commons/blob/master/version.md)**\n\n ## 定义实体类\n \nDSQL 是在 JPA 的基础上扩展而来的，因此它的用法与 JPA 非常相似。\n在定义实体类方面，可以说是完全相同，没有任何的区别，\n在本示例程序中，我们定义了一个 Address 的实体类，代码如下所示：\n\n```java\npackage com.terran4j.demo.dsql;\n\nimport javax.persistence.*;\n\n@Entity(name = \"demo_address\")\n@Table(indexes = {\n\t\t@Index(name = \"idx_gps\", columnList = \"lon,lat\"),\n        @Index(name = \"idx_name\", columnList = \"name\")\n})\npublic class Address {\n\n    public Address() {\n    }\n\n    public Address(String name, Double lon, Double lat) {\n        this.name = name;\n        this.lon = lon;\n        this.lat = lat;\n    }\n\n\t@Id\n\t@GeneratedValue\n\t@Column(length = 20)\n\tprivate Long id;\n\n    @Column(length = 100)\n    private String name;\n\n\t@Column(length = 20, precision = 8)\n\tprivate Double lon;\n\n\t@Column(length = 20, precision = 8)\n\tprivate Double lat;\n\n    // 省略 getter / setter  等方法\n}\n```\n\n仍然是 JPA 的规范。\n\n\n## 编写 JpaRepository\n\n由于 DSQL 依赖 JPA， 所以我们完全可以按JPA 的方式编写一个 Repository 类，\n代码如下所示：\n\n```java\n\npackage com.terran4j.demo.dsql;\n\nimport org.springframework.data.jpa.repository.JpaRepository;\n\npublic interface AddressDAO extends JpaRepository<Address, Long> {\n\n}\n\n```\n\n然后我们就可以写一个 main 函数测试一下了，代码如下所示：\n\n```java\npackage com.terran4j.demo.dsql.appjpa;\n\nimport com.terran4j.commons.test.DatabaseTestConfig;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.demo.dsql.Address;\nimport com.terran4j.demo.dsql.AddressDAO;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.domain.EntityScan;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@EntityScan(basePackageClasses = Address.class)\n@EnableJpaRepositories(basePackageClasses = AddressDAO.class)\n@Import(DatabaseTestConfig.class) // 自动装配默认的数据库配置。\n@SpringBootApplication\npublic class JpaDemoApplication implements ApplicationRunner {\n\n    private static final Logger log = LoggerFactory.getLogger(JpaDemoApplication.class);\n\n    @Autowired\n    private AddressDAO addressDAO;\n\n    @Override\n    public void run(ApplicationArguments appArgs) throws Exception {\n        // 清空表中的数据，以避免旧数据干扰运行。\n        addressDAO.deleteAll();\n\n        // 添加几条位置数据，以方便下面的查询。\n        List<Address> addresses = new ArrayList<>();\n        Address address1 = new Address(\"金域国际中心\",\n                116.3139456511, 40.0676693732);\n        addresses.add(address1);\n        Address address2 = new Address(\"龙泽地铁站\",\n                116.3193368912, 40.0707811250);\n        addresses.add(address2);\n        Address address3 = new Address(\"回龙观地铁站\",\n                116.3362830877, 40.0707770199);\n        addresses.add(address3);\n        addressDAO.save(addresses);\n\n        List<Address> result = addressDAO.findAll();\n        if (log.isInfoEnabled()) {\n            log.info(\"\\n查询结果：{}\", Strings.toString(result));\n        }\n    }\n\n    public static void main(String[] args) {\n        SpringApplication app = new SpringApplication(JpaDemoApplication.class);\n        app.setWebEnvironment(false);\n        app.run(args);\n        System.exit(0);\n    }\n}\n```\n\n注意：这个类上有一行代码：`@Import(DatabaseTestConfig.class)`，\nDatabaseTestConfig 是 terran4j-commons-test 子项目提供的一个配置类，\n它的目标是在写测试代码时，注入了一个默认的数据源配置，\n其作用相当于在 application.properties 文件中自动加入如下配置项：\n\n```\nspring.datasource.driverClassName = com.mysql.jdbc.Driver\nspring.datasource.url = jdbc:mysql://127.0.0.1:3306/test?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8\nspring.datasource.username = root\nspring.datasource.password = \nspring.jpa.hibernate.ddl-auto = create\nspring.jpa.show-sql = true\nspring.jpa.jackson.serialization.indent_output = true\n```\n\n这是一种约定优先的设计思想，如果你在本机开发时，本地数据库名为 test，\n用户名用 root，无密码（反正本地都是测试数据，无所谓安全性），\n如果你的本地环境符合这些配置约定时，那示例代码下下来无须任何配置就可以运行，\n否则您还是老老实实的在 application 文件中定义配置吧。\n\n最后我们运行下 main 函数，发现数据写入数据库中了，\n所以说 JPA 入手容易，写写简单的 CURD 还是非常容易的。\n\n \n## 编写 DsqlRepository \n\n以上都还只是 JPA 的知识，从本节开始 DSQL 要闪亮登场了。\n\n这一节，我们要实现一个查询功能：\n* 根据指定的位置，查询与其距离最近的一个位置。\n* 除了返回最近位置数据外，还要返回两个位置之间的距离，单位为米。\n这个 SQL 就有点小复杂了，我们用 DSQL 来实现。\n\n与 JPA 类似， DSQL 的 Repository 也是需要继承一个接口，代码如下所示：\n\n```java\npackage com.terran4j.demo.dsql;\n\nimport com.terran4j.commons.dsql.DsqlRepository;\nimport com.terran4j.commons.dsql.DsqlQuery;\nimport org.springframework.data.repository.query.Param;\n\nimport java.util.List;\n\npublic interface AddressDistanceDAO extends DsqlRepository<AddressDistance> {\n\n    @DsqlQuery(\"address-nearest\")\n    AddressDistance getNearest(\n            @Param(\"lat\") double lat, @Param(\"lon\") double lon);\n\n}\n```\n\n注意，这里的 @Param 仍复用了 JPA 提供的注解，\n但 @DsqlQuery 却是由 DSQL 所定义的，其值定义了一个动态 SQL 文件的名称，\n如上面的 @DsqlQuery(\"address-nearest\") 表示要在这个接口所在的包里面，\n定义一个名为 address-nearest.sql.ftl 的文件。\n之所以要用 .sql.ftl 作为文件的后缀名，是因为文件里面本质上是要写一段 SQL ，\n但可以用 Freemarker 的语法进行渲染，使其具备动态性，\n如 address-nearest.sql.ftl 文件内容如下：\n\n```ftl\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nORDER BY distance ASC\nlimit 0, 1\n```\n\n这段 SQL 用 ROUND 函数计算了当前位置与入参位置的距离，并且被命名为 distance ，\n这样就可以用 distance 字段排序了，整个逻辑都是用 SQL 实现。\n\n注意可以用 @{...} 来引用 DAO 方法中的入参，如上面的 @{lat}, @{lon} 等等。\n\n这段 SQL 查询出来的列，有 address 实体类中的所有列，还新多出来一个  distance 列，\n因此不能用 List<Address> 来接收，我们这里定义了一个名为  AddressDistance 的类：\n\n```java\npackage com.terran4j.demo.dsql;\n\nimport com.terran4j.commons.util.Strings;\n\npublic class AddressDistance {\n\n    // 位置记录\n    private Address address;\n\n    // 此位置与入参所指定位置的距离，单位为米。\n    private Long distance;\n\n    // 省略 getter / setter  等方法\n    \n    public String toString() {\n        return Strings.toString(this);\n    }\n\n}\n```\n\n也就是说，新的类可以复合之前已定义好的实体类，然后只要添加之前所没有字段就可以了，\nDSQL 非常的智能，可以根据数据库字段名自动映射到类的属性名，\n映射规则与 JPA 也完全一样，即从下划线命名法映射到驼峰命名法，\n例如，数据库字段名如果为 max_distance, 会自动映射到名为 maxDistance 的属性上。\n\n最后，我们编写一个新的 main 函数来测试一下：\n\n```java\npackage com.terran4j.demo.dsql.appdsql;\n\nimport com.terran4j.commons.dsql.EnableDsqlRepositories;\nimport com.terran4j.commons.test.DatabaseTestConfig;\nimport com.terran4j.demo.dsql.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.domain.EntityScan;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@EntityScan(basePackageClasses = Address.class)\n@EnableJpaRepositories(basePackageClasses = AddressDAO.class)\n@EnableDsqlRepositories(basePackageClasses = AddressDistanceDAO.class)\n@Import(DatabaseTestConfig.class) // 自动装配默认的数据库配置。\n@SpringBootApplication\npublic class DsqlDemoApplication implements ApplicationRunner {\n\n    private static final Logger log = LoggerFactory.getLogger(DsqlDemoApplication.class);\n\n    @Autowired\n    private AddressDAO addressDAO;\n\n    @Autowired\n    private AddressDistanceDAO addressDistanceDAO;\n\n    @Override\n    public void run(ApplicationArguments appArgs) throws Exception {\n        // 清空表中的数据，以避免旧数据干扰运行。\n        addressDAO.deleteAll();\n\n        // 添加几条位置数据，以方便下面的查询。\n        List<Address> addresses = new ArrayList<>();\n        Address address1 = new Address(\"金域国际中心\",\n                116.3139456511, 40.0676693732);\n        addresses.add(address1);\n        Address address2 = new Address(\"龙泽地铁站\",\n                116.3193368912, 40.0707811250);\n        addresses.add(address2);\n        Address address3 = new Address(\"回龙观地铁站\",\n                116.3362830877, 40.0707770199);\n        addresses.add(address3);\n        addressDAO.save(addresses);\n\n        // 当前位置，作为查询的参数\n        Address currentAddress = new Address(\"融泽嘉园一号院\",\n                116.3086509705, 40.0668729389);\n\n        AddressDistance addressDistance = addressDistanceDAO.getNearest(\n                currentAddress.getLat(), currentAddress.getLon());\n        if (log.isInfoEnabled()) {\n            log.info(\"\\n查询最近位置（指定参数名），当前位置：{},\\n最近位置： {}\",\n                    currentAddress, addressDistance);\n        }\n    }\n\n    public static void main(String[] args) {\n        SpringApplication app = new SpringApplication(DsqlDemoApplication.class);\n        app.setWebEnvironment(false);\n        app.run(args);\n        System.exit(0);\n    }\n}\n```\n\n注意： 在 private AddressDistanceDAO addressDistanceDAO 这一行可能会报错：\n\n```Could not autowire. No beans of 'AddressDistanceDAO' type found```\n\n原因是 IDEA 不能识别这个自动注入的 Bean，\n不过这个错不会影响程序的运行，可以按以下方法忽略掉：\n1. 在 IDEA 中打开： Settings -> Editor -> Inspections\n2. 在窗口中间找到 Spring -> Spring Core -> Code \n3. 点击 Autowiring for Bean Class 这一项，将右侧的 Severity 从 Error 改为 Weak Warning\n4. 点击右下方的 OK 按钮，以完成设置。\n\n注意，在具有 @SpringBootApplication 的类上指定\n\n```\n@EnableDsqlRepositories(basePackageClasses = AddressDistanceDAO.class)\n```\n\nSpring 容器就会扫描 AddressDistanceDAO 类所在的包，\n将这个包（不含子包）中所有继承了 DsqlRepository 接口的接口，都自动注入到容器中，\n这与 JPA 提供的 @EnableJpaRepositories 的用法非常的类似。\n\n最后，我们运行一下程序，可以看到控制台有这样的输出：\n```\n2018-02-20 19:54:27.722  INFO 10876 --- [           main] c.t.commons.dsql.impl.DsqlBuilder        : \nSQL（模板解析后）: \nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nORDER BY distance ASC\nlimit 0, 1\n参数: {lon=116.3086509705, lat=40.0668729389}\n2018-02-20 19:54:27.768  INFO 10876 --- [           main] com.terran4j.commons.util.Expressions    : parseExpression done: #lat\n2018-02-20 19:54:27.772  INFO 10876 --- [           main] com.terran4j.commons.util.Expressions    : parseExpression done: #lon\n2018-02-20 19:54:27.848  INFO 10876 --- [           main] c.t.commons.dsql.impl.DsqlExecutorImpl   : \nSQL（变量替换后）: \nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( ? * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( ? * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( ? * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nORDER BY distance ASC\nlimit 0, 1\n参数: [ 40.0668729389, 40.0668729389, 116.3086509705 ]\n```\n\nDSQL 会对 SQL 经过两次处理：\n* 第一次是用 FreeMarker 模板引擎对 .sql.ftl 文件中的内容进行解析，\n    获得解析后 SQL 。\n* 第二次是对形如 @{...} 的变量占位符进行变量替换后，获得最终可执行的 SQL 。\n这两次的 SQL 及对应的参数均会打印到日志中，以方便开发人员排查问题。\n\n\n# 省略 @Param 注解\n\n@Param 注解是可以省略的，省略后在 .sql.ftl 中参数只能以 arg0, arg1 的方式引用。\n如对于方法：\n\n```java\n    @DsqlQuery(\"address-nearest-2\")\n    AddressDistance getNearest2(double lat, double lon);\n``` \n\n对应的 .sql.ftl 就得写成这样：\n```ftl\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{arg0} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{arg0} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{arg1} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nORDER BY distance ASC\nlimit 0, 1\n```\n\n对 java 的反射有了解的朋友可能知道，java 编译成 .class 时参数名没有保留下来，\n因此反射调取参数名时，均是 arg0, arg1... 的形式。\n后来 java 1.8 时允许指定编译开关 javac -parameters ，以保留参数名，\n有兴趣的朋友们可以试试看。\n\n另外，如果方法的参数有且仅有一个，且没有 @Param 注解修饰，\n还可以用 args 表示这个参数，如对于 DAO 方法：\n\n```java\n    @DsqlQuery(\"address-list\")\n    List<AddressDistance> getAll(AddressQuery params);\n```\n\n其中 AddressQuery 类的定义为：\n\n```java\npackage com.terran4j.demo.dsql;\n\nimport com.terran4j.commons.util.Strings;\n\npublic class AddressQuery {\n\n    private Double lat;\n\n    private Double lon;\n\n    private String name;\n\n    private boolean nearFirst = true;\n\n    public AddressQuery(Double lat, Double lon) {\n        this.lat = lat;\n        this.lon = lon;\n    }\n\n    // 省略 getter / setter  toString 等方法\n}\n```\n\n对应的 .sql.ftl 文件内容可以这样写：\n\n```ftl\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{args.lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{args.lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{args.lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nwhere 1 = 1\n<#if args.name ??>\n    and name like @{args.name}\n</#if>\nORDER BY distance <#if args.nearFirst>ASC<#else>DESC</#if>\n```\n\n在 @{...} 中，可以用 Spring EL 表达式的写法，引用参数对象的属性，甚至属性的属性。\n只要是 Spring EL 表达式支持就可以。\n\n注意 <#if args.nearFirst>ASC<#else>DESC</#if> 这一行， \n<#if> 是 FreeMarker 的语法，表示分支判断。\nFreeMarker 是 Web 开发中非常常用的模板引擎工具，\n对 FreeMarker 不了解的朋友们，建议先在网上学习一下 FreeMarker 的语法，\n非常的简单，应该不到 30 分钟就可以掌握。 \n\n\n##  返回结果的类型映射\n\n当 DAO 接口继承 DsqlRepository 接口时，要求指定一个泛型类型，\n其方法可以有 3 种返回类型\n1. 指定的泛型类型的 List 类型， 这时 SQL 的查询结果可以有 0 到多条记录。\n2. 指定的泛型类型，这时 SQL 的查询结果必须是 0 到 1 条记录，多条时会报错。\n3. int 类型，这时 SQL 查询结果必须是一个数字，如： SELECT count(*) from ...  的形式。\n\n如下代码展示了不同返回值的方法：\n\n```java\npublic interface AddressDistanceDAO extends DsqlRepository<AddressDistance> {\n\n    @DsqlQuery(\"address-nearest\")\n    AddressDistance getNearest(@Param(\"lat\") double lat, @Param(\"lon\") double lon);\n\n    @DsqlQuery(\"address-list\")\n    List<AddressDistance> getAll(AddressQuery params);\n\n    @DsqlQuery(\"address-count\")\n    int count(@Param(\"lat\") double lat, @Param(\"lon\") double lon,\n              @Param(\"maxDistance\") int maxDistance);\n}\n```\n\n## 使用 @Modifying 进行增删改操作\n\n以上都是用 @Query 注解修饰查询方法，那如果我们要进行 insert / update / delete \n操作时怎么办呢？\n\n答案就是用 @Modifying 来修饰方法，我们看下面的代码：\n\n```java\npublic interface AddressDistanceDAO extends DsqlRepository<AddressDistance> {\n    \n    @DsqlModifying(\"address-update-nearest\")\n    int updateNearest(@Param(\"name\") String name,\n                      @Param(\"lat\") double lat, @Param(\"lon\") double lon);\n\n    @DsqlModifying(\"address-delete-nearest\")\n    int deleteNearest(@Param(\"lat\") double lat, @Param(\"lon\") double lon);\n}\n```\n\n与 updateNearest 相关的 address-update-nearest.sql.ftl 文件内容如下：\n\n```ftl\nUPDATE demo_address SET `name` = @{name}\nWHERE id IN (\n    SELECT t.id FROM (\n        SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n            POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n            + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n            * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n        )) * 1000) AS distance\n        FROM demo_address\n        ORDER BY distance ASC\n        LIMIT 0, 1\n    ) AS t\n)\n```\n\n与 deleteNearest 相关的 address-delete-nearest.sql.ftl 文件内容如下：\n\n```ftl\nDELETE from demo_address\nWHERE id IN (\n    SELECT t.id FROM (\n        SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n            POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n            + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n            * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n        )) * 1000) AS distance\n        FROM demo_address\n        ORDER BY distance ASC\n        LIMIT 0, 1\n    ) AS t\n)\n```\n\n代码语法都是一样的，就不多讲了。\n\n\n## 总结\n\nDSQL 定位是作为 JPA 的一个补充，提供了一种编写原生的、动态复杂 SQL 的方式，\n以提高开发效率和代码可维护性。\n\n希望对大家有帮助！"
  },
  {
    "path": "commons-dsql/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.github.terran4j</groupId>\n        <artifactId>terran4j-commons-parent</artifactId>\n        <version>1.0.4-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>terran4j-commons-dsql</artifactId>\n    <packaging>jar</packaging>\n    <name>terran4j-commons-dsql</name>\n    <url>https://github.com/terran4j/commons</url>\n\n    <dependencies>\n        <!-- 引入JPA -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-jpa</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>mysql</groupId>\n            <artifactId>mysql-connector-java</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-freemarker</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/DsqlExecutor.java",
    "content": "package com.terran4j.commons.dsql;\n\nimport com.terran4j.commons.dsql.impl.SqlInfo;\nimport com.terran4j.commons.util.error.BusinessException;\n\nimport java.util.List;\n\n/**\n * 动态 SQL 执行器。\n *\n * @author jiangwei\n */\npublic interface DsqlExecutor {\n\n    /**\n     * 按 query 查询，查询的结果，每行数据转成指定的对象。\n     *\n     * @param sqlInfo     SQL 及参数。\n     * @param elementType 返回的对象类型\n     * @param <T>         返回类型\n     * @return 查询记录对应的对象列表。\n     */\n    <T> List<T> query4List(SqlInfo sqlInfo, Class<T> elementType)\n            throws BusinessException;\n\n    /**\n     * @param sqlInfo SQL 及参数\n     * @return 查询记录数量。\n     * @throws BusinessException 查询出错。\n     */\n    int query4Count(SqlInfo sqlInfo) throws BusinessException;\n\n    /**\n     * @param sqlInfo SQL 及参数\n     * @return 受影响记录的数量。\n     * @throws BusinessException 执行出错。\n     */\n    int update(SqlInfo sqlInfo) throws BusinessException;\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/DsqlModifying.java",
    "content": "package com.terran4j.commons.dsql;\n\nimport java.lang.annotation.*;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.METHOD)\n@Documented\npublic @interface DsqlModifying {\n\n    String value();\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/DsqlQuery.java",
    "content": "package com.terran4j.commons.dsql;\n\nimport java.lang.annotation.*;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.METHOD)\n@Documented\npublic @interface DsqlQuery {\n\n    String value();\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/DsqlRepository.java",
    "content": "package com.terran4j.commons.dsql;\n\n/**\n * 动态 SQL Repository 接口。\n * @param <T> 表对应的类\n */\npublic interface DsqlRepository<T> {\n\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/EnableDsqlRepositories.java",
    "content": "package com.terran4j.commons.dsql;\n\nimport com.terran4j.commons.dsql.impl.DsqlRepositoryConfigRegistrar;\nimport org.springframework.context.annotation.Import;\n\nimport java.lang.annotation.*;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\n@Documented\n@Import(DsqlRepositoryConfigRegistrar.class)\npublic @interface EnableDsqlRepositories {\n\n    Class<?>[] value() default {};\n\n    Class<?>[] basePackageClasses() default {};\n\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/QueryBean.java",
    "content": "package com.terran4j.commons.dsql;\n\nimport com.terran4j.commons.util.Strings;\n\npublic class QueryBean {\n\n    public static final String wrapWithLike(String source) {\n        if (source == null) {\n            return null;\n        }\n        source = source.trim();\n        if (source.length() == 0) {\n            return \"%\";\n        }\n        if (source.indexOf(\"%\") >= 0) {\n            return source;\n        }\n        if (!source.startsWith(\"%\")) {\n            source = \"%\" + source;\n        }\n        if (!source.endsWith(\"%\")) {\n            source =  source + \"%\";\n        }\n        return source;\n    }\n\n    public String toString() {\n        return Strings.toString(this);\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/config/DsqlConfiguration.java",
    "content": "package com.terran4j.commons.dsql.config;\n\nimport com.terran4j.commons.dsql.impl.DsqlExecutorImpl;\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\n\n@ComponentScan(basePackageClasses = DsqlExecutorImpl.class)\n@Configuration\npublic class DsqlConfiguration {\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/impl/CompositeBeanRowMapper.java",
    "content": "package com.terran4j.commons.dsql.impl;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.BeanUtils;\nimport org.springframework.jdbc.core.BeanPropertyRowMapper;\nimport org.springframework.jdbc.core.RowMapper;\n\nimport javax.persistence.Entity;\nimport java.beans.PropertyDescriptor;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\n\npublic class CompositeBeanRowMapper<T> implements RowMapper<T> {\n\n    private static final Logger log = LoggerFactory.getLogger(CompositeBeanRowMapper.class);\n\n    private Class<T> mappedClass;\n\n    private BeanPropertyRowMapper<T> defaultMapper;\n\n    private final Map<String, BeanPropertyRowMapper> mappers = new HashMap<>();\n\n    private final Map<String, PropertyDescriptor> props = new HashMap<>();\n\n    public static <T> CompositeBeanRowMapper<T> newInstance(Class<T> mappedClass) {\n        return new CompositeBeanRowMapper<T>(mappedClass);\n    }\n\n    private CompositeBeanRowMapper(Class<T> mappedClass) {\n        initialize(mappedClass);\n    }\n\n    private void initialize(Class<T> mappedClass) {\n        this.mappedClass = mappedClass;\n        defaultMapper = BeanPropertyRowMapper.newInstance(mappedClass);\n        PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass);\n        for (PropertyDescriptor pd : pds) {\n            if (pd.getWriteMethod() == null) {\n                continue;\n            }\n            Class<?> propertyType = pd.getReadMethod().getReturnType();\n            Entity entity = propertyType.getAnnotation(Entity.class);\n            if (entity == null) {\n                continue;\n            }\n            String key = pd.getName();\n            props.put(key, pd);\n\n            BeanPropertyRowMapper<?> mapper = BeanPropertyRowMapper.newInstance(propertyType);\n            mappers.put(key, mapper);\n        }\n    }\n\n    @Override\n    public T mapRow(ResultSet rs, int rowNum) throws SQLException {\n        T result = defaultMapper.mapRow(rs, rowNum);\n        if (mappers.size() > 0) {\n            Iterator<String> it = mappers.keySet().iterator();\n            while(it.hasNext()) {\n                String key = it.next();\n                BeanPropertyRowMapper<?> mapper = mappers.get(key);\n                Object fieldObject = mapper.mapRow(rs, rowNum);\n                PropertyDescriptor pd = props.get(key);\n                Method writeMethod = pd.getWriteMethod();\n                try {\n                    writeMethod.invoke(result, fieldObject);\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    log.error(\"call setter failed while map row: \" + e.getMessage() + \"\\n\" +\n                            \"rs: \" + rs.toString() + \"\\n\" +\n                            \"fieldObject: \" + fieldObject + \"\\n\" +\n                            \"mappedClass: \" + mappedClass, e);\n                }\n            }\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/impl/DsqlBuilder.java",
    "content": "package com.terran4j.commons.dsql.impl;\n\nimport com.terran4j.commons.util.Encoding;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport freemarker.cache.ClassTemplateLoader;\nimport freemarker.cache.TemplateLoader;\nimport freemarker.template.Configuration;\nimport freemarker.template.Template;\nimport freemarker.template.TemplateException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.ui.freemarker.FreeMarkerTemplateUtils;\n\nimport java.io.IOException;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class DsqlBuilder {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(DsqlBuilder.class);\n\n\tprivate static DsqlBuilder instance = null;\n\n\tpublic static DsqlBuilder getInstance() {\n\t\tif (instance != null) {\n\t\t\treturn instance;\n\t\t}\n\n\t\tsynchronized (DsqlBuilder.class) {\n\t\t\tif (instance != null) {\n\t\t\t\treturn instance;\n\t\t\t}\n\t\t\tinstance = new DsqlBuilder();\n\t\t\treturn instance;\n\t\t}\n\t}\n\n\tprivate final Map<String, Template> templates = new HashMap<String, Template>();\n\n\tprivate final Configuration freeMarker;\n\n\tprivate DsqlBuilder() {\n\t\tsuper();\n\n\t\ttry {\n\t\t\tfreeMarker = new Configuration(Configuration.VERSION_2_3_25);\n\t\t\tfreeMarker.setDefaultEncoding(Encoding.UTF8.getName());\n\t\t\tTemplateLoader ctl = new ClassTemplateLoader(getClass(), \"/\");\n\t\t\tfreeMarker.setTemplateLoader(ctl);\n\t\t} catch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\n\tprivate String getPath(Class<?> clazz, String fileName) {\n\t\tString path = clazz.getPackage().getName().replace('.', '/')\n                + \"/\" + fileName;\n\t\treturn path;\n\t}\n\n\tprivate final Template getTemplate(Class<?> clazz, String fileName) {\n\t\tString path = getPath(clazz, fileName);\n\n\t\t// 检查文件是否存在。\n\t\tClassLoader loader = clazz.getClassLoader();\n\t\tURL url = loader.getResource(path);\n\t\tif (url == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tTemplate template = templates.get(path);\n\t\tif (template != null) {\n\t\t\treturn template;\n\t\t}\n\n\t\tsynchronized (DsqlBuilder.class) {\n\t\t\ttemplate = templates.get(path);\n\t\t\tif (template != null) {\n\t\t\t\treturn template;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\t\tlog.info(\"load freemarker template: {}\", path);\n\t\t\t\t}\n\t\t\t\ttemplate = freeMarker.getTemplate(path);\n\t\t\t\ttemplates.put(path, template);\n\t\t\t\treturn template;\n\t\t\t} catch (IOException e) {\n\t\t\t\tthrow new RuntimeException(e);\n\t\t\t}\n\t\t}\n\t}\n\n    private final String build(Template template, Map<String, Object> model) //\n\t\t\tthrows IOException, TemplateException {\n\t\tif (!templates.containsValue(template)) {\n\t\t\tString msg = \"Can't build from the template which NOT get by calling this method:\\n\"\n\t\t\t\t\t+ \"ClasspathFreeMarker.getTemplate(Class<?> clazz, String fileName)\";\n\t\t\tthrow new UnsupportedOperationException(msg);\n\t\t}\n\t\tString html = FreeMarkerTemplateUtils.processTemplateIntoString( //\n\t\t\t\ttemplate, model);\n\t\treturn html;\n\t}\n\n\tpublic final String buildSQL(Map<String, Object> model, Class<?> clazz, String sqlName) throws BusinessException {\n        if (model == null) {\n        \tmodel = new HashMap<>();\n\t\t}\n\n\t\tString fileName = sqlName + \".sql.ftl\";\n        Template template = getTemplate(clazz, fileName);\n        if (template == null) {\n            throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n                    .put(\"package\", clazz.getPackage())\n                    .put(\"fileName\", fileName)\n                    .setMessage(\"在包 ${package} 下面找不到文件： ${fileName}。\");\n        }\n\n        try {\n            String sql = build(template, model);\n            if (log.isInfoEnabled()) {\n                log.info(\"\\nSQL（模板解析后）: \\n{}\\n参数: {}\", sql.trim(), model);\n            }\n            return sql;\n        } catch (IOException | TemplateException e) {\n            throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n                    .put(\"package\", clazz.getPackage())\n                    .put(\"fileName\", fileName)\n                    .put(\"params\", model)\n                    .setMessage(\"使用文件 ${fileName} 构建SQL出错：\" + e.getMessage());\n        }\n    }\n\n\n    public final String buildPreparedArgs(String sql, List<String> keys) {\n        if (sql == null || sql.trim().length() == 0) {\n            throw new IllegalArgumentException(\"sql can't be null or empty.\");\n        }\n        sql = sql.trim();\n        if (keys == null || keys.size() > 0) {\n            throw new IllegalArgumentException(\"keys can't be null and must be an empty list.\");\n        }\n\n        final String begin = \"@{\";\n        final String end = \"}\";\n        StringBuffer sb = new StringBuffer();\n        final int size = sql.length();\n        final int beginLength = begin.length();\n        final int endLength = end.length();\n        int from = 0;\n        while (true) {\n            int m = sql.indexOf(begin, from); // 变量开始位置。\n            int n = sql.indexOf(end, from); // 变量结束位置。\n\n            if (m >= 0 && m < size && n > m && n < size) {\n                String s0 = sql.substring(from, m); // 变量之前的部分。\n                sb.append(s0);\n\n                // 变量定义部分。\n                String matchedText = sql.substring(m, n + endLength);\n\n                // 变量本身。\n                String key = matchedText.substring(beginLength, matchedText.length() - endLength);\n                keys.add(key);\n\n                sb.append(\"?\"); // 变量用“？”代替（SQL绑定变量的语法）。\n\n                from = n + endLength;\n            } else {\n                break;\n            }\n        }\n        if (from < size) {\n            sb.append(sql.substring(from));\n        }\n\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/impl/DsqlExecutorImpl.java",
    "content": "package com.terran4j.commons.dsql.impl;\n\nimport com.terran4j.commons.dsql.DsqlExecutor;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.core.RowMapper;\n\nimport java.util.List;\n\npublic class DsqlExecutorImpl implements DsqlExecutor {\n\n    private JdbcTemplate jdbcTemplate;\n\n    public DsqlExecutorImpl() {\n    }\n\n    public DsqlExecutorImpl(JdbcTemplate jdbcTemplate) {\n        this.jdbcTemplate = jdbcTemplate;\n    }\n\n    public JdbcTemplate getJdbcTemplate() {\n        return jdbcTemplate;\n    }\n\n    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {\n        this.jdbcTemplate = jdbcTemplate;\n    }\n\n    /**\n     * 按 query 查询，查询的结果，每行数据转成指定的对象。\n     *\n     * @param sqlInfo       查询用的 SQL 及参数。\n     * @param elementType 返回的对象类型\n     * @param <T> 返回的对象类型\n     * @return 将结果集转化成对象列表\n     */\n    @Override\n    public <T> List<T> query4List(SqlInfo sqlInfo, Class<T> elementType) {\n        RowMapper<T> rm = CompositeBeanRowMapper.newInstance(elementType);\n        List<T> result = jdbcTemplate.query(sqlInfo.getSql(), sqlInfo.getArgs(), rm);\n        return result;\n    }\n\n    @Override\n    public int query4Count(SqlInfo sqlInfo) throws BusinessException {\n        Integer count = jdbcTemplate.queryForObject(sqlInfo.getSql(), sqlInfo.getArgs(),\n                Integer.class);\n        if (count == null) {\n            throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n                    .put(\"sql\", sqlInfo.getSql())\n                    .setMessage(\"Query4Count SQL must return a number, for example:\\n\" +\n                            \"select count(*) from user;\\nbut SQL is: \\n{sql}\");\n        }\n        return count;\n    }\n\n    @Override\n    public int update(SqlInfo sqlInfo) throws BusinessException {\n        return jdbcTemplate.update(sqlInfo.getSql(), sqlInfo.getArgs());\n    }\n\n\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/impl/DsqlRepositoryBeanFactory.java",
    "content": "package com.terran4j.commons.dsql.impl;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.config.ConfigurableListableBeanFactory;\nimport org.springframework.beans.factory.support.BeanDefinitionRegistry;\nimport org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class DsqlRepositoryBeanFactory implements BeanDefinitionRegistryPostProcessor {\n\n    @Override\n    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)\n            throws BeansException {\n        // TODO:\n    }\n\n    @Override\n    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)\n            throws BeansException {\n        // do nothing.\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/impl/DsqlRepositoryConfigRegistrar.java",
    "content": "package com.terran4j.commons.dsql.impl;\n\nimport com.terran4j.commons.dsql.DsqlExecutor;\nimport com.terran4j.commons.dsql.DsqlRepository;\nimport com.terran4j.commons.dsql.EnableDsqlRepositories;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.reflect.InterfaceFilter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.config.BeanDefinition;\nimport org.springframework.beans.factory.support.*;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.context.annotation.ImportBeanDefinitionRegistrar;\nimport org.springframework.core.annotation.AnnotationAttributes;\nimport org.springframework.core.type.AnnotationMetadata;\nimport org.springframework.jdbc.core.JdbcTemplate;\n\nimport java.io.IOException;\nimport java.util.HashSet;\nimport java.util.Objects;\nimport java.util.Set;\n\npublic class DsqlRepositoryConfigRegistrar implements ImportBeanDefinitionRegistrar,\n        ApplicationContextAware {\n\n    private static final Logger log = LoggerFactory.getLogger(DsqlRepositoryConfigRegistrar.class);\n\n//    private static final String BEAN_NAME_DSQL_EXECUTOR = \"dsqlExecutor\";\n\n    private static ApplicationContext applicationContext = null;\n\n    private static DefaultListableBeanFactory beanFactory = null;\n\n    private static DsqlExecutorImpl executor = null;\n\n    public static final ApplicationContext getApplicationContext() {\n        return applicationContext;\n    }\n\n    public static DsqlExecutor getDsqlExecutor() {\n        if (beanFactory == null) {\n            throw new IllegalStateException(\"Spring BeanFactory NOT found, \" +\n                    \"maybe spring is not started yet.\");\n        }\n\n        if (executor != null) {\n            return executor;\n        }\n\n        synchronized (DsqlRepositoryConfigRegistrar.class) {\n            if (executor != null) {\n                return executor;\n            }\n            JdbcTemplate jdbcTemplate = beanFactory.getBean(JdbcTemplate.class);\n            executor = new DsqlExecutorImpl(jdbcTemplate);\n            return executor;\n        }\n    }\n\n    private final Set<Package> scannedPackages = new HashSet<>();\n\n    private final Set<Package> scannedClasses = new HashSet<>();\n\n    private final BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator();\n\n    @Override\n    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n        DsqlRepositoryConfigRegistrar.applicationContext = applicationContext;\n    }\n\n    @Override\n    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {\n        AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata\n                .getAnnotationAttributes(EnableDsqlRepositories.class.getName()));\n        Class<?>[] basePackageClasses = attributes.getClassArray(\"value\");\n        if (basePackageClasses == null || basePackageClasses.length == 0) {\n            basePackageClasses = attributes.getClassArray(\"basePackageClasses\");\n        }\n\n        if (registry instanceof DefaultListableBeanFactory) {\n            DefaultListableBeanFactory factory = (DefaultListableBeanFactory) registry;\n            if (beanFactory == null) {\n                beanFactory = factory; // 缓存 beanFactory 对象。\n            }\n\n            registerBeanDefinitions(factory, basePackageClasses);\n        } else {\n            String msg = String.format(\"Current registry %s is NOT the class: %s, \" +\n                            \"maybe the Spring Version is incompatible.\",\n                    registry.getClass(), DefaultListableBeanFactory.class);\n            throw new UnsupportedOperationException(msg);\n        }\n    }\n\n    void registerBeanDefinitions(DefaultListableBeanFactory registry, Class<?>[] basePackageClasses) {\n        if (basePackageClasses == null || basePackageClasses.length == 0) {\n            return;\n        }\n        if (log.isInfoEnabled()) {\n            log.info(\"register DsqlRepositories on basePackageClasses: \"\n                    + basePackageClasses);\n        }\n\n        for (Class<?> basePackageClass : basePackageClasses) {\n            Package currentPackage = basePackageClass.getPackage();\n            if (scannedPackages.contains(currentPackage)) {\n                continue;\n            }\n            Set<Class<?>> daoClasses = scanClasses(basePackageClass);\n            if (daoClasses == null || daoClasses.size() == 0) {\n                continue;\n            }\n            for (Class<?> daoClass : daoClasses) {\n                if (scannedClasses.contains(daoClass)) {\n                    continue;\n                }\n                registBean(registry, daoClass);\n            }\n        }\n    }\n\n    final void registBean(DefaultListableBeanFactory registry, Class<?> daoClass) {\n        if (log.isInfoEnabled()) {\n            log.info(\"regist DsqlRepository: {}\", daoClass);\n        }\n        BeanDefinition annotationProcessor = BeanDefinitionBuilder\n                .genericBeanDefinition(daoClass).getBeanDefinition();\n        String beanName = beanNameGenerator.generateBeanName(annotationProcessor, registry);\n        registerBeanDefinitionIfNotExists(registry, beanName, daoClass);\n    }\n\n    boolean registerBeanDefinitionIfNotExists(\n            DefaultListableBeanFactory registry, String beanName, Class<?> beanClass) {\n        if (registry.containsBeanDefinition(beanName)) {\n            return false;\n        }\n\n        String[] candidates = registry.getBeanDefinitionNames();\n        for (String candidate : candidates) {\n            BeanDefinition beanDefinition = registry.getBeanDefinition(candidate);\n            if (Objects.equals(beanDefinition.getBeanClassName(), beanClass.getName())) {\n                return false;\n            }\n        }\n\n        Object bean = DsqlRepositoryProxy.createProxyObject(beanClass);\n        registry.registerSingleton(beanName, bean);\n        return true;\n    }\n\n    final Set<Class<?>> scanClasses(Class<?> basePackageClass) {\n        InterfaceFilter filter = new InterfaceFilter(DsqlRepository.class);\n        try {\n            return Classes.scanClasses(basePackageClass, false,\n                    filter, basePackageClass.getClassLoader());\n        } catch (IOException | ClassNotFoundException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/impl/DsqlRepositoryProxy.java",
    "content": "package com.terran4j.commons.dsql.impl;\n\nimport com.terran4j.commons.dsql.DsqlExecutor;\nimport com.terran4j.commons.dsql.DsqlModifying;\nimport com.terran4j.commons.dsql.DsqlQuery;\nimport com.terran4j.commons.dsql.DsqlRepository;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.springframework.cglib.proxy.Enhancer;\nimport org.springframework.cglib.proxy.MethodInterceptor;\nimport org.springframework.cglib.proxy.MethodProxy;\nimport org.springframework.core.LocalVariableTableParameterNameDiscoverer;\nimport org.springframework.data.repository.query.Param;\nimport org.springframework.util.StringUtils;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Parameter;\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.terran4j.commons.dsql.impl.DsqlRepositoryConfigRegistrar.getDsqlExecutor;\n\npublic class DsqlRepositoryProxy implements MethodInterceptor {\n\n    public static final <T> T createProxyObject(Class<T> clazz) {\n        // 增强器，动态代码生成器\n        Enhancer enhancer = new Enhancer();\n\n        // 回调方法\n        DsqlRepositoryProxy proxy = new DsqlRepositoryProxy(clazz);\n        enhancer.setCallback(proxy);\n\n        // 设置生成类的父类类型\n        enhancer.setSuperclass(clazz);\n        // 动态生成字节码并返回代理对象\n        return (T) enhancer.create();\n    }\n\n    private Class<?> elementType;\n\n    private final Class<?> proxyInterface;\n\n    private final LocalVariableTableParameterNameDiscoverer paramNameDiscoverer\n            = new LocalVariableTableParameterNameDiscoverer();\n\n    private DsqlRepositoryProxy(Class<?> proxyInterface) {\n        this.proxyInterface = proxyInterface;\n        this.elementType = getElementType(proxyInterface);\n    }\n\n    public Class<?> getElementType(Class<?> proxyInterface) {\n        Type[] types = proxyInterface.getGenericInterfaces();\n        if (types == null || types.length != 1) {\n            throw new RuntimeException(proxyInterface + \" must implement ONLY ONE interface: \" +\n                    DsqlRepository.class);\n        }\n        Type type = types[0];\n        ParameterizedType parameterizedType = (ParameterizedType) type;\n        Type genericType = parameterizedType.getActualTypeArguments()[0];\n        if (genericType instanceof Class<?>) {\n            return (Class<?>) genericType;\n        } else {\n            throw new RuntimeException(proxyInterface + \"'s genericType is NOT a class: \" +\n                    genericType);\n        }\n    }\n\n    @Override\n    public Object intercept(Object o, Method method, Object[] objects,\n                            MethodProxy methodProxy) throws Throwable {\n\n        DsqlExecutor executor = getDsqlExecutor();\n        if (executor == null) {\n            throw new IllegalStateException(\"DsqlExecutor is NOT registed in Application.\");\n        }\n\n        Map<String, Object> context = getContext(method, objects);\n\n        DsqlQuery query = method.getAnnotation(DsqlQuery.class);\n        if (query != null) {\n            String sqlName = query.value();\n            if (StringUtils.isEmpty(sqlName)) {\n                sqlName = method.getName();\n            }\n            return doQuery(sqlName, context, method, executor);\n        }\n\n        DsqlModifying modifying = method.getAnnotation(DsqlModifying.class);\n        if (modifying != null) {\n            String sqlName = modifying.value();\n            if (StringUtils.isEmpty(sqlName)) {\n                sqlName = method.getName();\n            }\n            return doModifying(sqlName, context, method, executor);\n        }\n\n        String methodName = method.getName();\n        if (methodName.startsWith(\"query\") || methodName.startsWith(\"get\")\n                || methodName.startsWith(\"list\") || methodName.startsWith(\"find\")\n                || methodName.startsWith(\"select\") || methodName.startsWith(\"read\")\n                || methodName.startsWith(\"count\") || methodName.startsWith(\"load\")) {\n            return doQuery(methodName, context, method, executor);\n        }\n\n        if (methodName.startsWith(\"update\") || methodName.startsWith(\"delete\")\n                || methodName.startsWith(\"remove\") || methodName.startsWith(\"edit\")\n                || methodName.startsWith(\"write\") || methodName.startsWith(\"modify\")\n                || methodName.startsWith(\"change\") || methodName.startsWith(\"create\")\n                || methodName.startsWith(\"set\") || methodName.startsWith(\"alter\")) {\n            return doModifying(methodName, context, method, executor);\n        }\n\n        String msg = \"在接口 %s 中的 %s 方法状态不正确：\\n\" +\n                \"要么方法上用 @DsqlQuery 或 @DsqlModifying 注解修饰；\\n\" +\n                \"要么方法名用约定的单词开头：\\n\" +\n                \"如果是查询类操作，用 query,get,list,find,select,read,count,load 开头；\\n\" +\n                \"如果是修改/删除类操作，用 update,delete,remove,edit,write,\" +\n                \"modify,change,create,set,alter 开头。\";\n        throw new IllegalStateException(msg);\n    }\n\n    private Object doModifying(String sqlName, Map<String, Object> args,\n                               Method method, DsqlExecutor executor) throws BusinessException {\n        SqlInfo sqlInfo = SqlInfo.create(args, proxyInterface, sqlName);\n\n        Class<?> returnType = method.getReturnType();\n        if (returnType.equals(Integer.class) || returnType.equals(int.class)) {\n            return executor.update(sqlInfo);\n        }\n        if (returnType.equals(Long.class) || returnType.equals(long.class)) {\n            return (long) executor.update(sqlInfo);\n        }\n\n        throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n                .put(\"method\", method).put(\"returnType\", returnType)\n                .setMessage(\"Unknown returnType: ${returnType}, \" +\n                        \"@DsqlModifying method ONLY support  returnTypes: \" +\n                        \"Long, long, Integer, int\");\n    }\n\n    private Object doQuery(String sqlName, Map<String, Object> args,\n                           Method method, DsqlExecutor executor) throws BusinessException {\n        SqlInfo sqlInfo = SqlInfo.create(args, proxyInterface, sqlName);\n\n        Class<?> returnType = method.getReturnType();\n        if (returnType.equals(Integer.class) || returnType.equals(int.class)) {\n            return executor.query4Count(sqlInfo);\n        }\n        if (returnType.equals(Long.class) || returnType.equals(long.class)) {\n            return executor.query4Count(sqlInfo);\n        }\n        if (returnType.equals(elementType)) {\n            List<?> result = executor.query4List(sqlInfo, elementType);\n            if (result == null || result.size() == 0) {\n                return null;\n            }\n            if (result.size() > 1) {\n                throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n                        .put(\"resultsCount\", result.size())\n                        .setMessage(\"Too many Results, expect no more than 1 \" +\n                                \"but has ${count}\");\n            }\n            return result.get(0);\n        }\n        if (List.class.equals(returnType)) {\n            List<?> result = executor.query4List(sqlInfo, elementType);\n            return result;\n        }\n        throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n                .put(\"method\", method).put(\"returnType\", returnType)\n                .put(\"elementType\", elementType.getSimpleName())\n                .setMessage(\"Unknown returnType: ${returnType}, \" +\n                        \"@DsqlQuery method ONLY support  returnTypes: \" +\n                        \"List<${elementType}>, ${elementType}, \" +\n                        \"Long, long, Integer, int.\");\n    }\n\n    private Map<String, Object> getContext(Method method, Object[] args) throws BusinessException {\n        Map<String, Object> context = new HashMap<>();\n\n        // 没有参数的情况。\n        Parameter[] params = method.getParameters();\n        if (params == null || params.length == 0) {\n            return context;\n        }\n\n        // 只有一个参数，并且没有 @Param 注解时，用 query 作默认的 key.\n        if (params.length == 1 && params[0].getAnnotation(Param.class) == null) {\n            if (args[0] != null) {\n                context.put(\"args\", args[0]);\n            }\n        }\n\n        String[] paramNames = paramNameDiscoverer.getParameterNames(method);\n        for (int i = 0; i < params.length; i++) {\n            Parameter param = params[i];\n            String key = param.getName();\n\n            // 利用 Spring 提供的工具，获取参数名。\n            if (paramNames != null && paramNames.length >= i + 1) {\n                key = paramNames[i];\n            }\n\n            Param paramAnnotation = param.getAnnotation(Param.class);\n            if (paramAnnotation != null) {\n                key = paramAnnotation.value();\n            }\n\n            Object value = args[i];\n            if (value != null) {\n                context.put(key, value);\n            }\n        }\n\n        return context;\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/main/java/com/terran4j/commons/dsql/impl/SqlInfo.java",
    "content": "package com.terran4j.commons.dsql.impl;\n\nimport com.terran4j.commons.util.Expressions;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\n\npublic class SqlInfo {\n\n    private static final Logger log = LoggerFactory.getLogger(DsqlExecutorImpl.class);\n\n    private String sql;\n\n    private Object[] args;\n\n    public String getSql() {\n        return sql;\n    }\n\n    public void setSql(String sql) {\n        this.sql = sql;\n    }\n\n    public Object[] getArgs() {\n        return args;\n    }\n\n    public void setArgs(Object[] args) {\n        this.args = args;\n    }\n\n    @Override\n    public String toString() {\n        return \"SqlInfo{\" +\n                \"sql='\" + sql + '\\'' +\n                \", args=\" + Arrays.toString(args) +\n                '}';\n    }\n\n    public static SqlInfo create(Map<String, Object> context, Class<?> basePackageClass,\n                                 String sqlName) throws BusinessException {\n        DsqlBuilder dsqlBuilder = DsqlBuilder.getInstance();\n        String sql = dsqlBuilder.buildSQL(context, basePackageClass, sqlName);\n\n        List<String> keys = new ArrayList<>();\n        sql = dsqlBuilder.buildPreparedArgs(sql, keys);\n\n        Object[] args = new Object[keys.size()];\n        int i = 0;\n        for (String key : keys) {\n            String el = \"#\" + key;\n            Object value = Expressions.parse(el, context);\n            args[i] = value;\n            i++;\n        }\n\n        SqlInfo info = new SqlInfo();\n        info.setArgs(args);\n        info.setSql(sql);\n        if (log.isInfoEnabled()) {\n            log.info(\"\\nSQL（变量替换后）: \\n{}\\n参数: {}\", sql.trim(), Strings.toString(args));\n        }\n        return info;\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/Address.java",
    "content": "package com.terran4j.demo.dsql;\n\nimport javax.persistence.*;\n\n@Entity(name = \"demo_address\")\n@Table(indexes = {\n\t\t@Index(name = \"idx_gps\", columnList = \"lon,lat\"),\n        @Index(name = \"idx_name\", columnList = \"name\")\n})\npublic class Address {\n\n    public Address() {\n    }\n\n    public Address(String name, Double lon, Double lat) {\n        this.name = name;\n        this.lon = lon;\n        this.lat = lat;\n    }\n\n    /**\n\t * id, 自增主键\n\t */\n\t@Id\n\t@GeneratedValue\n\t@Column(length = 20)\n\tprivate Long id;\n\n    @Column(length = 100)\n    private String name;\n\n\t@Column(length = 20, precision = 8)\n\tprivate Double lon;\n\n\t@Column(length = 20, precision = 8)\n\tprivate Double lat;\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public Double getLon() {\n        return lon;\n    }\n\n    public void setLon(Double lon) {\n        this.lon = lon;\n    }\n\n    public Double getLat() {\n        return lat;\n    }\n\n    public void setLat(Double lat) {\n        this.lat = lat;\n    }\n}"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/AddressDAO.java",
    "content": "package com.terran4j.demo.dsql;\n\nimport org.springframework.data.jpa.repository.JpaRepository;\n\npublic interface AddressDAO extends JpaRepository<Address, Long> {\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/AddressDistance.java",
    "content": "package com.terran4j.demo.dsql;\n\nimport com.terran4j.commons.util.Strings;\n\npublic class AddressDistance {\n\n    // 位置记录\n    private Address address;\n\n    // 此位置与入参所指定位置的距离，单位为米。\n    private Long distance;\n\n    public Address getAddress() {\n        return address;\n    }\n\n    public void setAddress(Address address) {\n        this.address = address;\n    }\n\n    public Long getDistance() {\n        return distance;\n    }\n\n    public void setDistance(Long distance) {\n        this.distance = distance;\n    }\n\n    public String toString() {\n        return Strings.toString(this);\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/AddressDistanceDAO.java",
    "content": "package com.terran4j.demo.dsql;\n\nimport com.terran4j.commons.dsql.DsqlModifying;\nimport com.terran4j.commons.dsql.DsqlQuery;\nimport com.terran4j.commons.dsql.DsqlRepository;\nimport org.springframework.data.repository.query.Param;\n\nimport java.util.List;\n\npublic interface AddressDistanceDAO extends DsqlRepository<AddressDistance> {\n    \n    @DsqlQuery(\"address-nearest\")\n    AddressDistance getNearest(@Param(\"lat\") double lat, @Param(\"lon\") double lon);\n\n    @DsqlQuery(\"address-nearest-2\")\n    AddressDistance getNearest2(double lat, double lon);\n\n    @DsqlQuery(\"address-list\")\n    List<AddressDistance> getAll(AddressQuery params);\n\n    @DsqlQuery(\"address-count\")\n    int count(@Param(\"lat\") double lat, @Param(\"lon\") double lon,\n              @Param(\"maxDistance\") int maxDistance);\n\n    @DsqlModifying(\"address-update-nearest\")\n    int updateNearest(@Param(\"name\") String name,\n                      @Param(\"lat\") double lat, @Param(\"lon\") double lon);\n\n    @DsqlModifying(\"address-delete-nearest\")\n    int deleteNearest(@Param(\"lat\") double lat, @Param(\"lon\") double lon);\n}"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/AddressQuery.java",
    "content": "package com.terran4j.demo.dsql;\n\nimport com.terran4j.commons.util.Strings;\n\npublic class AddressQuery {\n\n    private Double lat;\n\n    private Double lon;\n\n    private String name;\n\n    private boolean nearFirst = true;\n\n    public AddressQuery(Double lat, Double lon) {\n        this.lat = lat;\n        this.lon = lon;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public boolean isNearFirst() {\n        return nearFirst;\n    }\n\n    public void setNearFirst(boolean nearFirst) {\n        this.nearFirst = nearFirst;\n    }\n\n    public Double getLat() {\n        return lat;\n    }\n\n    public void setLat(Double lat) {\n        this.lat = lat;\n    }\n\n    public Double getLon() {\n        return lon;\n    }\n\n    public void setLon(Double lon) {\n        this.lon = lon;\n    }\n\n    public final String toString() {\n        return Strings.toString(this);\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/address-count.sql.ftl",
    "content": "\nSELECT count(*) from (\n    SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n        POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n        + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n        * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n    )) * 1000) AS distance\n    FROM test_location\n) as t\nwhere distance < @{maxDistance}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/address-delete-nearest.sql.ftl",
    "content": "\nDELETE from demo_address\nWHERE id IN (\n    SELECT t.id FROM (\n        SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n            POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n            + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n            * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n        )) * 1000) AS distance\n        FROM demo_address\n        ORDER BY distance ASC\n        LIMIT 0, 1\n    ) AS t\n)"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/address-list.sql.ftl",
    "content": "\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{args.lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{args.lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{args.lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nwhere 1 = 1\n<#if args.name ??>\n    and name like @{args.name}\n</#if>\nORDER BY distance <#if args.nearFirst>ASC<#else>DESC</#if>\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/address-nearest-2.sql.ftl",
    "content": "\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{arg0} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{arg0} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{arg1} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nORDER BY distance ASC\nlimit 0, 1"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/address-nearest.sql.ftl",
    "content": "\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM demo_address\nORDER BY distance ASC\nlimit 0, 1"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/address-update-nearest.sql.ftl",
    "content": "\nUPDATE demo_address SET `name` = @{name}\nWHERE id IN (\n    SELECT t.id FROM (\n        SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n            POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n            + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n            * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n        )) * 1000) AS distance\n        FROM demo_address\n        ORDER BY distance ASC\n        LIMIT 0, 1\n    ) AS t\n)"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/appdsql/DsqlDemoApplication.java",
    "content": "package com.terran4j.demo.dsql.appdsql;\n\nimport com.terran4j.commons.dsql.EnableDsqlRepositories;\nimport com.terran4j.commons.test.DatabaseTestConfig;\nimport com.terran4j.demo.dsql.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.domain.EntityScan;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@EntityScan(basePackageClasses = Address.class)\n@EnableJpaRepositories(basePackageClasses = AddressDAO.class)\n@EnableDsqlRepositories(basePackageClasses = AddressDistanceDAO.class)\n@Import(DatabaseTestConfig.class) // 自动装配默认的数据库配置。\n@SpringBootApplication\npublic class DsqlDemoApplication implements ApplicationRunner {\n\n    private static final Logger log = LoggerFactory.getLogger(DsqlDemoApplication.class);\n\n    @Autowired\n    private AddressDAO addressDAO;\n\n    @Autowired\n    private AddressDistanceDAO addressDistanceDAO;\n\n    @Override\n    public void run(ApplicationArguments appArgs) throws Exception {\n//        // 清空表中的数据，以避免旧数据干扰运行。\n//        addressDAO.deleteAll();\n//\n//        // 添加几条位置数据，以方便下面的查询。\n//        List<Address> addresses = new ArrayList<>();\n//        Address address1 = new Address(\"金域国际中心\",\n//                116.3139456511, 40.0676693732);\n//        addresses.add(address1);\n//        Address address2 = new Address(\"龙泽地铁站\",\n//                116.3193368912, 40.0707811250);\n//        addresses.add(address2);\n//        Address address3 = new Address(\"回龙观地铁站\",\n//                116.3362830877, 40.0707770199);\n//        addresses.add(address3);\n//        addressDAO.save(addresses);\n//\n//        // 当前位置，作为查询的参数\n//        Address currentAddress = new Address(\"融泽嘉园一号院\",\n//                116.3086509705, 40.0668729389);\n//\n//        AddressDistance addressDistance = addressDistanceDAO.getNearest(\n//                currentAddress.getLat(), currentAddress.getLon());\n//        if (log.isInfoEnabled()) {\n//            log.info(\"\\n查询最近位置（指定参数名）： {}\", addressDistance);\n//        }\n//\n//        addressDistance = addressDistanceDAO.getNearest2(\n//                currentAddress.getLat(), currentAddress.getLon());\n//        if (log.isInfoEnabled()) {\n//            log.info(\"\\n查询最近位置（不指定参数名）： {}\",\n//                    currentAddress, addressDistance);\n//        }\n//\n//        AddressQuery args = new AddressQuery(\n//                currentAddress.getLat(), currentAddress.getLon());\n//        args.setName(\"%地铁%\");\n//\n//        args.setNearFirst(true);\n//        List<AddressDistance> result = addressDistanceDAO.getAll(args);\n//        if (log.isInfoEnabled()) {\n//            log.info(\"\\n由近及远查询所有，入参： {}, \\n查询结果： {}\", args, result);\n//        }\n//\n//        args.setNearFirst(false);\n//        result = addressDistanceDAO.getAll(args);\n//        if (log.isInfoEnabled()) {\n//            log.info(\"\\n由远及近查询所有，入参： {}, \\n查询结果： {}\", args, result);\n//        }\n//\n//        int maxDistance = 5000;\n//        long count = addressDistanceDAO.count(\n//                currentAddress.getLat(), currentAddress.getLon(), maxDistance);\n//        if (log.isInfoEnabled()) {\n//            log.info(\"\\n统计指定范围内的位置数量，count = {}\", count);\n//        }\n//\n//        addressDistanceDAO.updateNearest(\"新位置\",\n//                currentAddress.getLat(), currentAddress.getLon());\n//        addressDistance = addressDistanceDAO.getNearest(\n//                currentAddress.getLat(), currentAddress.getLon());\n//        if (log.isInfoEnabled()) {\n//            log.info(\"\\n查询最近位置（修改后）： {}\", addressDistance);\n//        }\n//\n//        addressDistanceDAO.deleteNearest(\n//                currentAddress.getLat(), currentAddress.getLon());\n//        addressDistance = addressDistanceDAO.getNearest(\n//                currentAddress.getLat(), currentAddress.getLon());\n//        if (log.isInfoEnabled()) {\n//            log.info(\"\\n查询最近位置（删除后）： {}\", addressDistance);\n//        }\n    }\n\n    public static void main(String[] args) {\n        SpringApplication app = new SpringApplication(DsqlDemoApplication.class);\n        app.setWebEnvironment(false);\n        app.run(args);\n        System.exit(0);\n    }\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/demo/dsql/appjpa/JpaDemoApplication.java",
    "content": "package com.terran4j.demo.dsql.appjpa;\n\nimport com.terran4j.commons.test.DatabaseTestConfig;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.demo.dsql.Address;\nimport com.terran4j.demo.dsql.AddressDAO;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.domain.EntityScan;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@EntityScan(basePackageClasses = Address.class)\n@EnableJpaRepositories(basePackageClasses = AddressDAO.class)\n@Import(DatabaseTestConfig.class) // 自动装配默认的数据库配置。\n@SpringBootApplication\npublic class JpaDemoApplication implements ApplicationRunner {\n\n    private static final Logger log = LoggerFactory.getLogger(JpaDemoApplication.class);\n\n    @Autowired\n    private AddressDAO addressDAO;\n\n    @Override\n    public void run(ApplicationArguments appArgs) throws Exception {\n        // 清空表中的数据，以避免旧数据干扰运行。\n//        addressDAO.deleteAll();\n//\n//        // 添加几条位置数据，以方便下面的查询。\n//        List<Address> addresses = new ArrayList<>();\n//        Address address1 = new Address(\"金域国际中心\",\n//                116.3139456511, 40.0676693732);\n//        addresses.add(address1);\n//        Address address2 = new Address(\"龙泽地铁站\",\n//                116.3193368912, 40.0707811250);\n//        addresses.add(address2);\n//        Address address3 = new Address(\"回龙观地铁站\",\n//                116.3362830877, 40.0707770199);\n//        addresses.add(address3);\n//        addressDAO.save(addresses);\n//\n//        List<Address> result = addressDAO.findAll();\n//        if (log.isInfoEnabled()) {\n//            log.info(\"\\n查询结果：{}\", Strings.toString(result));\n//        }\n    }\n\n    public static void main(String[] args) {\n        SpringApplication app = new SpringApplication(JpaDemoApplication.class);\n        app.setWebEnvironment(false);\n        app.run(args);\n        System.exit(0);\n    }\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/BaseDsqlTest.java",
    "content": "package com.terran4j.test.dsql;\n\nimport com.terran4j.commons.dsql.DsqlExecutor;\nimport com.terran4j.commons.dsql.EnableDsqlRepositories;\nimport com.terran4j.commons.dsql.config.DsqlConfiguration;\nimport com.terran4j.commons.dsql.impl.DsqlExecutorImpl;\nimport com.terran4j.commons.test.BaseSpringBootTest;\nimport com.terran4j.commons.test.DatabaseInitializer;\nimport com.terran4j.commons.test.DatabaseTestConfig;\nimport com.terran4j.commons.test.TruncateTable;\nimport com.terran4j.test.dsql.dao.Location;\nimport com.terran4j.test.dsql.dao.LocationDAO;\nimport com.terran4j.test.dsql.dao1.LocationDsqlDAO;\nimport com.terran4j.test.dsql.dao2.LocationDistanceDAO;\nimport com.terran4j.test.dsql.dao3.DistancedLocationDAO;\nimport com.terran4j.test.dsql.dao4.LocationAutoDAO;\nimport org.junit.Before;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.domain.EntityScan;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.data.jpa.repository.config.EnableJpaRepositories;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.test.context.TestExecutionListeners;\n\n@TruncateTable(basePackageClass = Location.class)\n@SpringBootTest(\n        webEnvironment = SpringBootTest.WebEnvironment.NONE,\n        classes = {BaseDsqlTest.Application.class}\n)\n@TestExecutionListeners({DatabaseInitializer.class})\npublic abstract class BaseDsqlTest extends BaseSpringBootTest {\n\n    @EntityScan(basePackageClasses = Location.class)\n    @EnableJpaRepositories(basePackageClasses = LocationDAO.class)\n    @EnableDsqlRepositories(basePackageClasses = {\n            LocationDsqlDAO.class,\n            LocationDistanceDAO.class,\n            DistancedLocationDAO.class,\n            LocationAutoDAO.class\n    })\n    @Import({\n            DsqlConfiguration.class,\n            DatabaseTestConfig.class,\n    })\n    @SpringBootApplication\n    public static class Application {\n    }\n\n    @Autowired\n    protected JdbcTemplate jdbcTemplate;\n\n    @Autowired\n    protected LocationDAO locationDAO;\n\n    protected DsqlExecutor dsqlExecutor;\n\n    protected Location loc1 = new Location(\"金域国际中心\",\n            116.3139456511, 40.0676693732);\n\n    protected Location loc2 = new Location(\"融泽嘉园一号院南门\",\n            116.3086509705, 40.0668729389);\n\n    Location loc3 = new Location(\"龙泽地铁站\",\n            116.3193368912, 40.0707811250);\n\n    @Before\n    public void setUp() {\n        locationDAO.save(loc1);\n        locationDAO.save(loc2);\n        DsqlExecutorImpl executor = new DsqlExecutorImpl(jdbcTemplate);\n        this.dsqlExecutor = executor;\n    }\n\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/DsqlAutoTest.java",
    "content": "package com.terran4j.test.dsql;\n\nimport com.terran4j.test.dsql.dao.Location;\nimport com.terran4j.test.dsql.dao.LocationQuery;\nimport com.terran4j.test.dsql.dao2.LocationDistance;\nimport com.terran4j.test.dsql.dao2.LocationDistanceDAO;\nimport com.terran4j.test.dsql.dao3.DistancedLocation;\nimport com.terran4j.test.dsql.dao3.DistancedLocationDAO;\nimport com.terran4j.test.dsql.dao4.LocationAutoDAO;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\n\nimport java.util.List;\n\npublic class DsqlAutoTest extends BaseDsqlTest {\n\n    private static final Logger log = LoggerFactory.getLogger(DsqlAutoTest.class);\n\n    @Autowired\n    protected LocationAutoDAO locationAutoDAO;\n\n    @Test\n    public void testGetLocations() throws Exception {\n        LocationQuery query = new LocationQuery();\n        query.setLon(loc3.getLon());\n        query.setLat(loc3.getLat());\n        List<Location> results = locationAutoDAO.getLocations(query);\n        log.info(\"results:\\n{}\", results);\n        Assert.assertEquals(2, results.size());\n        Assert.assertEquals(loc1.getName(), results.get(0).getName());\n        Assert.assertEquals(loc2.getName(), results.get(1).getName());\n    }\n\n    @Test\n    public void testCountLocation() throws Exception {\n        LocationQuery query = new LocationQuery();\n        query.setLon(loc3.getLon());\n        query.setLat(loc3.getLat());\n        int size = locationAutoDAO.countLocation(query);\n        Assert.assertEquals(1, size);\n    }\n\n    @Test\n    public void testGetNearestLocation() throws Exception {\n        Location location = locationAutoDAO.getNearestLocation(loc1.getLat(), loc1.getLon());\n        Assert.assertNotNull(location);\n        Assert.assertEquals(loc1.getName(), location.getName());\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/DsqlModifyingTest.java",
    "content": "package com.terran4j.test.dsql;\n\nimport com.terran4j.test.dsql.dao1.LocationDsqlDAO;\nimport com.terran4j.test.dsql.dao2.LocationDistance;\nimport com.terran4j.test.dsql.dao2.LocationDistanceDAO;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\n\npublic class DsqlModifyingTest extends BaseDsqlTest {\n\n    private static final Logger log = LoggerFactory.getLogger(DsqlModifyingTest.class);\n\n    @Autowired\n    private LocationDsqlDAO locationDsqlDAO;\n\n    @Autowired\n    private LocationDistanceDAO locationDistanceDAO;\n\n    @Test\n    public void testUpdate() throws Exception {\n        log.info(\"testUpdate\");\n        LocationDistance nearest = locationDistanceDAO.getNearest(\n                loc3.getLat(), loc3.getLon());\n        Assert.assertEquals(\"金域国际中心\", nearest.getLocation().getName());\n\n        String newName = \"最近位置\";\n        int rowCount = locationDsqlDAO.updateNearest(newName, loc3.getLat(), loc3.getLon());\n        Assert.assertEquals(1, rowCount);\n\n        nearest = locationDistanceDAO.getNearest(\n                loc3.getLat(), loc3.getLon());\n        Assert.assertEquals(newName, nearest.getLocation().getName());\n    }\n\n    @Test\n    public void testDelete() throws Exception {\n        log.info(\"testDelete\");\n        LocationDistance nearest = locationDistanceDAO.getNearest(\n                loc3.getLat(), loc3.getLon());\n        Assert.assertEquals(loc1.getName(), nearest.getLocation().getName());\n\n        int rowCount = locationDsqlDAO.deleteNearest(loc3.getLat(), loc3.getLon());\n        Assert.assertEquals(1, rowCount);\n\n        nearest = locationDistanceDAO.getNearest(\n                loc3.getLat(), loc3.getLon());\n        Assert.assertEquals(loc2.getName(), nearest.getLocation().getName());\n    }\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/DsqlQueryTest.java",
    "content": "package com.terran4j.test.dsql;\n\nimport com.terran4j.test.dsql.dao.*;\nimport com.terran4j.test.dsql.dao2.LocationDistance;\nimport com.terran4j.test.dsql.dao2.LocationDistanceDAO;\nimport com.terran4j.test.dsql.dao3.DistancedLocation;\nimport com.terran4j.test.dsql.dao3.DistancedLocationDAO;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\n\nimport java.util.List;\n\npublic class DsqlQueryTest extends BaseDsqlTest {\n\n    private static final Logger log = LoggerFactory.getLogger(DsqlQueryTest.class);\n\n    @Autowired\n    private LocationDistanceDAO locationDistanceDAO;\n\n    @Autowired\n    private DistancedLocationDAO distancedLocationDAO;\n\n    @Test\n    public void testQueryWithCompositeEntity() throws Exception {\n        LocationQuery query = new LocationQuery();\n        query.setLon(loc3.getLon());\n        query.setLat(loc3.getLat());\n        List<LocationDistance> results = locationDistanceDAO.query(query);\n        if (log.isInfoEnabled()) {\n            log.info(\"results:\\n{}\", results);\n        }\n        Assert.assertEquals(2, results.size());\n        Assert.assertEquals(loc1.getName(), results.get(0).getLocation().getName());\n        Assert.assertEquals(new Long(575), results.get(0).getDistance());\n        Assert.assertEquals(loc2.getName(), results.get(1).getLocation().getName());\n        Assert.assertEquals(new Long(1009), results.get(1).getDistance());\n    }\n\n    @Test\n    public void testQueryWithExtendEntity() throws Exception {\n        LocationQuery query = new LocationQuery();\n        query.setLon(loc3.getLon());\n        query.setLat(loc3.getLat());\n        List<DistancedLocation> results = distancedLocationDAO.query(query);\n        if (log.isInfoEnabled()) {\n            log.info(\"results:\\n{}\", results);\n        }\n        Assert.assertEquals(2, results.size());\n        Assert.assertEquals(loc1.getName(), results.get(0).getName());\n        Assert.assertEquals(new Long(575), results.get(0).getDistance());\n        Assert.assertEquals(loc2.getName(), results.get(1).getName());\n        Assert.assertEquals(new Long(1009), results.get(1).getDistance());\n    }\n\n    @Test\n    public void testQueryWithParamAnno() throws Exception {\n        LocationDistance nearestLocation = locationDistanceDAO.getNearest(\n                loc3.getLat(), loc3.getLon());\n        if (log.isInfoEnabled()) {\n            log.info(\"nearestLocation:\\n{}\", nearestLocation);\n        }\n        Assert.assertEquals(loc1.getName(), nearestLocation.getLocation().getName());\n        Assert.assertEquals(new Long(575), nearestLocation.getDistance());\n    }\n\n    @Test\n    public void testCount() throws Exception {\n        LocationQuery query = new LocationQuery();\n        query.setLon(loc3.getLon());\n        query.setLat(loc3.getLat());\n        int size = distancedLocationDAO.count(query);\n        Assert.assertEquals(1, size);\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao/Location.java",
    "content": "package com.terran4j.test.dsql.dao;\n\nimport javax.persistence.*;\n\n@Entity(name = \"test_location\")\n@Table(indexes = { //\n\t\t@Index(name = \"ux_location\", columnList = \"lon,lat\", unique = true)\n})\npublic class Location {\n\n    public Location() {\n    }\n\n    public Location(String name, Double lon, Double lat) {\n        this.name = name;\n        this.lon = lon;\n        this.lat = lat;\n    }\n\n    /**\n\t * id, 自增主键\n\t */\n\t@Id\n\t@GeneratedValue\n\t@Column(length = 20)\n\tprivate Long id;\n\n    @Column(length = 100)\n    private String name;\n\n\t@Column(length = 20, precision = 8)\n\tprivate Double lon;\n\n\t@Column(length = 20, precision = 8)\n\tprivate Double lat;\n\n    public Long getId() {\n        return id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public Double getLon() {\n        return lon;\n    }\n\n    public void setLon(Double lon) {\n        this.lon = lon;\n    }\n\n    public Double getLat() {\n        return lat;\n    }\n\n    public void setLat(Double lat) {\n        this.lat = lat;\n    }\n}"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao/LocationDAO.java",
    "content": "package com.terran4j.test.dsql.dao;\n\nimport org.springframework.data.jpa.repository.JpaRepository;\n\npublic interface LocationDAO extends JpaRepository<Location, Long> {\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao/LocationQuery.java",
    "content": "package com.terran4j.test.dsql.dao;\n\npublic class LocationQuery {\n\n    private Double lat;\n\n    private Double lon;\n\n    private boolean nearFirst = true;\n\n    public boolean isNearFirst() {\n        return nearFirst;\n    }\n\n    public void setNearFirst(boolean nearFirst) {\n        this.nearFirst = nearFirst;\n    }\n\n    public Double getLat() {\n        return lat;\n    }\n\n    public void setLat(Double lat) {\n        this.lat = lat;\n    }\n\n    public Double getLon() {\n        return lon;\n    }\n\n    public void setLon(Double lon) {\n        this.lon = lon;\n    }\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao1/LocationDsqlDAO.java",
    "content": "package com.terran4j.test.dsql.dao1;\n\nimport com.terran4j.commons.dsql.DsqlRepository;\nimport com.terran4j.commons.dsql.DsqlModifying;\nimport com.terran4j.test.dsql.dao.Location;\nimport org.springframework.data.repository.query.Param;\n\npublic interface LocationDsqlDAO extends DsqlRepository<Location> {\n\n    @DsqlModifying(\"update-nearest\")\n    int updateNearest(@Param(\"name\") String name,\n                      @Param(\"lat\") double lat, @Param(\"lon\") double lon);\n\n    @DsqlModifying(\"delete-nearest\")\n    int deleteNearest(@Param(\"lat\") double lat, @Param(\"lon\") double lon);\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao1/delete-nearest.sql.ftl",
    "content": "\nDELETE from test_location\nWHERE id IN (\n    SELECT t.id FROM (\n        SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n            POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n            + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n            * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n        )) * 1000) AS distance\n        FROM test_location\n        ORDER BY distance ASC\n        LIMIT 0, 1\n    ) AS t\n)"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao1/update-nearest.sql.ftl",
    "content": "\nUPDATE test_location SET `name` = @{name}\nWHERE id IN (\n    SELECT t.id FROM (\n        SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n            POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n            + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n            * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n        )) * 1000) AS distance\n        FROM test_location\n        ORDER BY distance ASC\n        LIMIT 0, 1\n    ) AS t\n)"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao2/LocationDistance.java",
    "content": "package com.terran4j.test.dsql.dao2;\n\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.test.dsql.dao.Location;\n\npublic class LocationDistance  {\n\n    private Location location;\n\n    private Long distance;\n\n    public Location getLocation() {\n        return location;\n    }\n\n    public void setLocation(Location location) {\n        this.location = location;\n    }\n\n    public Long getDistance() {\n        return distance;\n    }\n\n    public void setDistance(Long distance) {\n        this.distance = distance;\n    }\n\n    public String toString() {\n        return Strings.toString(this);\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao2/LocationDistanceDAO.java",
    "content": "package com.terran4j.test.dsql.dao2;\n\nimport com.terran4j.commons.dsql.DsqlQuery;\nimport com.terran4j.commons.dsql.DsqlRepository;\nimport com.terran4j.test.dsql.dao.LocationQuery;\nimport org.springframework.data.repository.query.Param;\n\nimport java.util.List;\n\npublic interface LocationDistanceDAO extends DsqlRepository<LocationDistance> {\n\n    @DsqlQuery(\"locations\")\n    List<LocationDistance> query(LocationQuery query);\n\n    @DsqlQuery(\"location-nearest\")\n    LocationDistance getNearest(@Param(\"lat\") double lat, @Param(\"lon\") double lon);\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao2/location-nearest.sql.ftl",
    "content": "\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM test_location\nORDER BY distance ASC\nlimit 0, 1"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao2/locations.sql.ftl",
    "content": "\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{args.lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{args.lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{args.lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM test_location\nORDER BY distance <#if args.nearFirst>ASC<#else>DESC</#if>\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao3/DistancedLocation.java",
    "content": "package com.terran4j.test.dsql.dao3;\n\nimport com.terran4j.test.dsql.dao.Location;\n\npublic class DistancedLocation extends Location {\n\n    private Long distance;\n\n    public Long getDistance() {\n        return distance;\n    }\n\n    public void setDistance(Long distance) {\n        this.distance = distance;\n    }\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao3/DistancedLocationDAO.java",
    "content": "package com.terran4j.test.dsql.dao3;\n\nimport com.terran4j.commons.dsql.DsqlQuery;\nimport com.terran4j.commons.dsql.DsqlRepository;\nimport com.terran4j.test.dsql.dao.LocationQuery;\n\nimport java.util.List;\n\npublic interface DistancedLocationDAO extends DsqlRepository<DistancedLocation> {\n\n    @DsqlQuery(\"locations\")\n    List<DistancedLocation> query(LocationQuery query);\n\n    @DsqlQuery(\"location-count\")\n    int count(LocationQuery query);\n\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao3/location-count.sql.ftl",
    "content": "\nSELECT count(*) from (\n    SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n        POW(SIN(( @{args.lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n        + COS( @{args.lat} * PI() / 180) * COS(lat * PI() / 180)\n        * POW(SIN(( @{args.lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n    )) * 1000) AS distance\n    FROM test_location\n) as t\nwhere t.distance > 1000\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao3/locations.sql.ftl",
    "content": "\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{args.lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{args.lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{args.lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM test_location\nORDER BY distance <#if args.nearFirst>ASC<#else>DESC</#if>\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao4/LocationAutoDAO.java",
    "content": "package com.terran4j.test.dsql.dao4;\n\nimport com.terran4j.commons.dsql.DsqlRepository;\nimport com.terran4j.test.dsql.dao.Location;\nimport com.terran4j.test.dsql.dao.LocationQuery;\nimport org.springframework.data.repository.query.Param;\n\nimport java.util.List;\n\npublic interface LocationAutoDAO extends DsqlRepository<Location> {\n\n    List<Location> getLocations(LocationQuery query);\n\n    int countLocation(LocationQuery query);\n\n    Location getNearestLocation(@Param(\"lat\") Double lat, @Param(\"lon\") Double lon);\n}\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao4/countLocation.sql.ftl",
    "content": "\nSELECT count(*) from (\n    SELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n        POW(SIN(( @{args.lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n        + COS( @{args.lat} * PI() / 180) * COS(lat * PI() / 180)\n        * POW(SIN(( @{args.lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n    )) * 1000) AS distance\n    FROM test_location\n) as t\nwhere t.distance > 1000\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao4/getLocations.sql.ftl",
    "content": "\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{args.lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{args.lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{args.lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM test_location\nORDER BY distance <#if args.nearFirst>ASC<#else>DESC</#if>\n"
  },
  {
    "path": "commons-dsql/src/test/java/com/terran4j/test/dsql/dao4/getNearestLocation.sql.ftl",
    "content": "\nSELECT *, ROUND(6378.137 * 2 * ASIN(SQRT(\n    POW(SIN(( @{lat} * PI() / 180 - lat * PI() / 180) / 2),2)\n    + COS( @{lat} * PI() / 180) * COS(lat * PI() / 180)\n    * POW(SIN(( @{lon} * PI() / 180 - lon * PI() / 180) / 2), 2)\n)) * 1000) AS distance\nFROM test_location\nORDER BY distance ASC\nLIMIT 0,1\n"
  },
  {
    "path": "commons-hedis/.gitignore",
    "content": "/target/\n/.classpath\n/.project\n*.iml\n"
  },
  {
    "path": "commons-hedis/README.md",
    "content": "\n本项目基于 Redis 提供两个常用的功能：\n1. 分布式锁；\n2. 轻量级分布式任务调度；\n\n此项目已经放到 github 中，需要源码的朋友请点击\n[这里](https://github.com/terran4j/commons/tree/master/commons-hedis)\n\n## 目录\n\n* 项目背景\n* 引入 Hedis 依赖\n* 在 application.yml 中配置 Redis \n* 启用 Hedis 服务\n* 使用 CacheService 缓存服务\n* 实现分布式同步\n* 实现轻量级分布式定时调度\n\n## 项目背景\n\nRedis 是一个高性能的 key-value 数据库。 \nRedis的出现，很大程度补偿了 memcache 这类 key-value 存储的不足，\n在部分场合可以对关系数据库起到很好的补充作用。\n\nRedis 的最常用的功能就是缓存数据了，不过它提供的功能已经超越了缓存系统，\n我们可以用 Redis 做更多的事情。\n\n本项目的目标是为了提供一个更简单易用的 Redis 客户端框架，\n它集成了 spring-data-redis， Jedis，Redisson 等多种优秀的 Redis 框架，\n并针对具体的场景，提供了更方便的使用方式。\n\n本项目被命名为 Hedis ，是 Happy for using Redis 之意，希望大家用 Redis 用得更爽。\n\n\n\n## 引入 Hedis 依赖\n\n如果是 maven ，请在 pom.xml 中添加依赖，如下所示：\n\n```xml\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-hedis</artifactId>\n            <version>${hedis.version}</version>\n        </dependency>\n```\n\n如果是 gradle，请在 build.gradle 中添加依赖，如下所示：\n\n```groovy\ncompile \"com.github.terran4j:terran4j-commons-hedis:${hedis.version}\"\n```\n\n${hedis.version} **最新稳定版，请参考 [这里](https://github.com/terran4j/commons/blob/master/version.md)**\n\n本教程的示例代码在 src/test/java 目录的 com.terran4j.demo.hedis 包中，\n您也可以从 [这里](https://github.com/terran4j/commons/tree/master/commons-hedis/src/test/java/com/terran4j/demo/hedis) 获取到。\n\n\n## 在 application.yml 中配置 Redis \n\n在 Spring Boot 应用程序中的 application.yml 文件中配置 Redis 服务器的信息，\n如下所示：\n\n```yaml\n\nspring:\n  redis:\n    host: 127.0.0.1\n    port: 6379\n    password: asd123\n    pool:\n      max-total: 8\n      max-idle: 8\n      min-idle: 0\n      max-wait: 1\n      \n```\n\n其中：\n* host： 表示 Redis 服务器的 IP 或域名，不配置默认是 127.0.0.1 ；\n* port： 表示 Redis 服务的端口，默认是 6379 ；\n* password： 表示连接 Redis 服务端的认证密码，默认为空（即不需要密码）；\n* pool.max-total： 表示 Redis 客户端连接池的总大小，不配置时默认是 8 ；\n* pool.max-idle： 表示 Redis 客户端连接池中连接的最大空闲数，\n    当空闲连接数大于此值时，多余的连接会释放掉，\n    当配置大于连接池总大小，会自动设置成等于连接池总大小；\n* pool.min-idle： 表示 Redis 客户端连接池中连接的最大空闲数，\n    不能超过最大空闲数的一半，当不配置时默认是 0 ；\n* pool.max-wait： 表示 Redis 客户端获取连接时的最大超时时间，单位为毫秒，\n    不配置默认是 -1 ，表示永不超时；\n* pool.max-total： 表示 Redis 客户端连接池的大小，不配置默认是 8 ；\n* cache.defaultExpirationSecond： 表示在 Spring 缓存管理器中，\n    默认的缓存过期时间，单位为秒，不配置时默认为 30 秒。\n\n\n# 启用 Hedis 服务\n\n\n首先，我们需要在有 @SpringBootApplication 注解的类上，添加 @EnableHedis 注解，\n以启用 Hedis 服务，如下代码所示：\n\n```java\n\npackage com.terran4j.demo.hedis;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.hedis.config.EnableHedis;\n\n@EnableHedis\n@SpringBootApplication\npublic class HedisDemoApp {\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication app = new SpringApplication(HedisDemoApp.class);\n\t\tapp.setWebEnvironment(false);\n\t\tapp.run(args);\n\t}\n\n}\n\n``` \n\n注意，启动 Hedis 服务后， Hedis 会按配置去连接 Redis 服务器，\n请确保 Redis 服务器是开启的并配置正确。\n\n\n# 使用 CacheService 缓存服务\n\nHedis 向 Spring 容器中注册了一个 CacheService 服务，\n它提供读写缓存数据的常用接口方法，CacheService 的用法如下代码所示：\n\n```java\n\npackage com.terran4j.demo.hedis;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Date;\n\n@Service\npublic class DemoCacheService implements ApplicationRunner {\n\n    private static final Logger log = LoggerFactory.getLogger(DemoCacheService.class);\n\n    @Autowired\n    private CacheService cacheService;\n\n    @Override\n    public void run(ApplicationArguments args) throws Exception {\n        log.info(\"演示如何使用 CacheService 服务\");\n\n        //  向缓存写入对象。\n        String key = \"u1\";\n        User user = new User(1, \"neo\", new Date());\n        cacheService.setObject(key, user, null);\n\n        // 从 缓存中读取对象。\n        User value = cacheService.getObject(key, User.class);\n        log.info(\"cache key = {}, value = {}\", key, value);\n    }\n\n}\n\n```\n\nCacheService 的实现是基于 Spring 提供的 RedisTemplate 类。\n对象的“序列化 / 反序列化”是用的 Json 的方式，因而具有较高的灵活性。\n\nCacheService 提供了以下常用的缓存方法，如：\n* 读取 / 写入 单个对象；\n* 读取 / 写入 整个 Map 对象； \n* 读取 / 写入 一个 Map 中的单个条目。\n\n这对于大多数缓存的使用场景已经足够了，若需要更多的 Redis 接口，可以直接引用：\n* RedisTemplate<String, String> \n* Jedis\n\n这两个服务之一（推荐用 RedisTemplate），里面有更多的接口方法可用。\n\n\n## 实现分布式同步\n\nHedis 还集成了 [Redisson](https://blog.csdn.net/u014042066/article/details/72778440) 开源项目，\nRedisson 基于 Redis 提供了很多强大的功能，其中就包括“分布式锁”。\n\n用 Redis 实现分布式锁的原理，请参看 [这里](http://ifeve.com/redis-lock/) 。\n简单来说，就是在执行同步块代码之前，先用 SETNX 操作向 Redis 写入一个 key - value 记录，\n写入成功，表示获取到对应的锁，就可以真正执行同步块中的代码；\n执行完成后，就删除这个 key - value 记录，表示释放了锁。\n（当然，在分布式环境下，实际的算法其实比这个复杂得多）\n\n而 Hedis 则在 Redisson 的基础上，让分布式锁的使用变得非常简单。\n\n只需要在 Spring Bean 的方法上加上 `@DSynchronized`  注解，\n就可以对这个方法用分布式锁实现同步，如下代码所示：\n\n```java\n\npackage com.terran4j.demo.hedis;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport com.terran4j.commons.hedis.dsyn.DSynchronized;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.repository.query.Param;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class CountService {\n\n    @Autowired\n    private CacheService cacheService;\n\n    /**\n     * 对 incrementAndGet 方法加上分布式并发控制。\n     */\n    @DSynchronized(\"'incrementAndGet-' + #key\")\n    public int incrementAndGet(@Param(\"key\") String key) {\n        return doIncrementAndGet(key);\n    }\n    \n    /**\n     * 一个没有并发控制的递增计算，需要调用方避免并发访问。\n     */\n    private int doIncrementAndGet(String key) {\n        // 从 Redis 缓存中取出计数器变量：\n        Integer counter;\n        try {\n            counter = cacheService.getObject(key, Integer.class);\n            if (counter == null) {\n                counter = 0;\n            }\n        } catch (BusinessException e1) {\n            throw new RuntimeException(e1);\n        }\n\n        // 故意让线程休眠一段时间，让并发问题更严重。\n        try {\n            Thread.sleep(100);\n        } catch (InterruptedException e) {\n            // ignore.\n        }\n\n        // 在本地让计数器变量加 1：\n        counter++;\n\n        // 将变量写回 Redis 缓存：\n        cacheService.setObject(key, counter, null);\n        return counter;\n    }\n\n}\n\n```\n\n@DSynchronized 类似于 java 提供的 synchronized 关键字，不过不同的是：\nsynchronized 只是在单个 JVM 范围内实现了多线程同步，而 @DSynchronized\n是在应用的整个集群范围内，也就是多个节点之间实现了多线程同步。\n\n在 @DSynchronized 中，可以用 Spring EL 表达式来定义申请分布式锁的 key ，\n如上面的 `'incrementAndGet-' + #key` ，两个单引号 '' 包裹的是字符串常量，\n用 # 号引用的是变量，用 + 号可以把这两部分拼接起来，这些都遵守\n[Spring EL 表达式](https://www.jianshu.com/p/25dcb764654e) 的语法。\n在方法中可以用 `@Param` 注解来使参数值成为 Spring EL 中的变量值。\n\n这个在 @DSynchronized 中的 Spring EL 表达式，以后我们称为“锁表达式”。\n\n如下面的代码：\n\n```java\n\n@Service\npublic class CountService {\n    \n    // 省略其它代码......\n    \n    @DSynchronized(\"'incrementAndGet-' + #key\")\n    public int incrementAndGet(@Param(\"key\") String key) {\n        return doIncrementAndGet(key);\n    }\n    \n}\n```\n\n方法 incrementAndGet 的锁表达式为： 'incrementAndGet-' + #key 。\n比如调用时参数 key = \"k1\"，则会在调用前申请一个名为 \"incrementAndGet-k1\" 的锁，\n申请到这个锁了，才能继续执行此方法，没有申请到锁就只能等待别人执行完后释放锁。\n\n也就是说，每次调用方法，都会根据锁表达式来生成一个 key，key 值相同才会竞争锁，\n如果有参数值的参与，这个锁的粒度就更细了，因获取不到锁而被阻塞的概率就会小很多。\n\n注意：请根据您的业务逻辑小心的定义锁表达式，请尽量的让锁的粒度更细。\n当然，你也可以图省事而不定义锁表达式，如下代码所示：\n\n```java\n\n@Service\npublic class CountService {\n    \n    // 省略其它代码......\n    \n    @DSynchronized\n    public int incrementAndGet(String key) {\n        return doIncrementAndGet(key);\n    }\n    \n}\n\n```\n\n这样 Hedis 会自动生成锁表达式 , 格式为：`${类名}::${方法名}(${参数类型列表})`，\n如上面的示例代码，生成的锁表达式为： \n```\ncom.terran4j.demo.hedis.CountService::incrementAndGet(java.lang.String)\n```\n这样生成的锁有两个问题：\n1. 代码重构（如包名、类名、方法名改了）后锁名就自动变了，\n    重上线时原来申请到的锁会失效。\n2. 一个线程申请到锁后，会阻塞住所有调用此方法的其它线程，性能可能会非常差。\n\n从概念上讲，这有点像数据库中的表锁，如果带上了参数，就像是行锁，性能当然更好了。\n\n因此：`强烈建议仔细的定义锁表达式，千万不要图省事而省略掉它`。\n\n当然，对方法的同步虽然简单，也能满足于大多数同步的需求，\n但还是有不少场景对分布式锁有更复杂的操作逻辑，\n你可以直接引用`RedissonClient` （Hedis 已经将它注入到 Spring 容器中）来实现。\n\n\n## 实现轻量级分布式定时调度\n\n在业务系统中，我们经常需要在后台执行一些定时任务调度，\n用 Spring Scheduling 框架可以很容易的实现，如下代码所示：\n\n```java\n\npackage com.terran4j.demo.hedis;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class LoopIncrementJob {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(LoopIncrementJob.class);\n\n\tprivate static final String key = \"demo3-counter\";\n\n\t@Autowired\n\tprivate CountService countService;\n\n\t@Scheduled(cron = \"0/1 * * * * *\")\n\tpublic void loopIncrement() {\n\t\tint count = countService.doIncrementAndGet(key);\n\t\tlog.info(\"\\nloopIncrement, counter = {}\", count);\n\t}\n\n}\n\n```\n\n@Scheduled 是 Spring Scheduling 框架提供的注解，cron = \"0/1 * * * * *\" 是每秒执行一次的意思，\n因此 @Scheduled(cron = \"0/1 * * * * *\") 的意思就是：\n Spring Scheduling 框架会每秒调用一次 loopIncrement() 方法。\n \n但 Spring Scheduling 框架是不支持分布式的，也就是说：\n当应用程序部署到一个多个节点时，每个节点都会独立的执行这个定时调度，\n如果代码中要操作共享的资源时，可能会出问题。\n \n业界有许多提供分布式任务调度的开源项目，如： quartz、TBSchedule、elastic-job、Saturn 等，\n但都比较重量级，如：资源占用多，部署复杂，学习成本高。\n所以 Hedis 提供了一种非常简单的方式进行分布式调度，\n简单到只加一个 @DScheduling 注解就搞定了，如下代码所示：\n \n ```java\n\npackage com.terran4j.demo.hedis;\n\nimport com.terran4j.commons.hedis.dschedule.DScheduling;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class LoopIncrementJob {\n\n    private static final Logger log = LoggerFactory.getLogger(LoopIncrementJob.class);\n\n    private static final String key = \"demo3-scheduling-counter\";\n\n    @Autowired\n    private CountService countService;\n\n    @DScheduling(lockExpiredSecond = 3)\n    @Scheduled(cron = \"0/1 * * * * *\")\n    public void loopIncrement() {\n        int count = countService.doIncrementAndGet(key);\n        log.info(\"\\nloopIncrement, counter = {}\", count);\n    }\n\n}\n\n```\n \n@DScheduling 必须修饰在有 @Scheduled 修饰的方法上，\n表示对这个定时任务采用分布式调度，它的算法为：\n1. 每次调度执行此方法（如上面的 loopIncrement() ）前，都尝试申请一个分布式锁；\n2. 如果没有获取到锁（通常意味其它节点获取到锁了），则本节点跳过此次执行。\n3. 如果获取到锁了，还会检查当前时间与上次的执行时间的时间差：\n* 如果此时间差超过了调度周期，则执行本次调度，并记录本次的执行时间（到 Redis 中）；\n* 如果此时间差未超过调度周期，则跳过本次调度。\n\n所谓“调度周期”，是指两次相邻的调度执行的时间差，如 cron = \"0/1 * * * * *\"\n表示每秒调度一次，则“调度周期”为 1 秒。\n@DScheduling 中有一个 lockExpiredSecond 属性，表示分布锁的过期时间，\n建议比调度周期略长，并且要确保任务的执行时间要远小于此 lockExpiredSecond \n所定义的时长。\n\n\n## 声明式缓存\n\n[Spring Cache](https://blog.csdn.net/poorcoder_/article/details/55258253) 提供了声明式缓存，\n即在方法上定义一些注解即可对数据进行缓存。\n\n而 Hedis 注入了 RedisCacheManager 服务，因此可以直接使用 Spring Cache 。\n\n（具体怎么使用 Spring Cache，本文就不过多介绍了，有问题请自行百度！）"
  },
  {
    "path": "commons-hedis/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.github.terran4j</groupId>\n        <artifactId>terran4j-commons-parent</artifactId>\n        <version>1.0.4-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>terran4j-commons-hedis</artifactId>\n    <packaging>jar</packaging>\n    <name>terran4j-commons-hedis</name>\n    <url>https://github.com/terran4j/commons/tree/master/commons-hedis</url>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-cache</artifactId>\n        </dependency>\n\t\t<dependency>\n            <groupId>org.aspectj</groupId>\n            <artifactId>aspectjweaver</artifactId>\n        </dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apache.commons</groupId>\n\t\t\t<artifactId>commons-lang3</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.google.code.gson</groupId>\n\t\t\t<artifactId>gson</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.google.guava</groupId>\n\t\t\t<artifactId>guava</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.fasterxml.jackson.core</groupId>\n\t\t\t<artifactId>jackson-databind</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>com.github.terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-util</artifactId>\n\t\t</dependency>\n\n        <dependency>\n            <groupId>org.redisson</groupId>\n            <artifactId>redisson</artifactId>\n        </dependency>\n\t\t\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-test</artifactId>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>redis.clients</groupId>\n\t\t\t<artifactId>jedis</artifactId>\n\t\t</dependency>\n\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/cache/CacheService.java",
    "content": "package com.terran4j.commons.hedis.cache;\n\nimport java.util.Map;\n\nimport com.terran4j.commons.util.error.BusinessException;\n\n/**\n * key - value 结构的缓存服务接口。<br>\n * 具体实现可能有多种，如：本地内存缓存、Redis 分布式缓存等。<br>\n * @author wei.jiang\n */\npublic interface CacheService {\n\t\n\t/**\n\t * 是否存在缓存的key。\n\t * @param key\n\t * @return\n\t */\n\tboolean existed(String key);\n\t\n\t/**\n\t * 根据 key 删除缓存。\n\t * @param key\n\t */\n\tvoid remove(String key);\n\n\t/**\n\t * 存一个键值对，不管键存不存在都能写成功。\n\t * @param key 键\n\t * @param value 值\n\t * @param expiredMillisecond 过期时间，以毫秒为单位。\n\t */\n\tvoid setObject(String key, Object value, Long expiredMillisecond);\n\t\n\t/**\n\t * 存一个键值对，只有键不存在才能写成功。\n\t * @param key 键\n\t * @param value 值\n\t * @param expiredMillisecond 过期时间，以毫秒为单位，不可以为空。\n\t * @return true表示键不存在且写成功， false表示键存在且不写入。\n\t */\n\tboolean setObjectIfAbsent(String key, Object value, Long expiredMillisecond);\n\n\t/**\n\t * 根据键，获取值对象。\n\t * @param key 键\n\t * @param clazz 值的类型。\n\t * @return 值对象\n\t */\n\t<T> T getObject(String key, Class<T> clazz) throws BusinessException;\n\t\n\t/**\n\t * 在一个 Hash 中设置键值对。\n\t * @param key Hash对象本身的键\n\t * @param entryKey Hash对象里面的键。\n\t * @param entryValue Hash对象里面的值。\n\t * @param expiredMillisecond 过期时间，以毫秒为单位。\n\t */\n\tvoid setHashEntry(String key, String entryKey, Object entryValue, Long expiredMillisecond);\n\t\n\t/**\n\t * 根据键，获取一个 Hash 中的值对象。\n\t * @param key Hash对象本身的键\n\t * @param entryKey Hash对象里面的键。\n\t * @param clazz 值的类型。\n\t */\n\t<T> T getHashEntry(String key, String entryKey, Class<T> clazz) throws BusinessException;\n\t\n\t/**\n\t * 设置一个整体的 Hash 对象。\n\t * @param key Hash对象本身的键\n\t * @param map Hash对象。\n\t * @param clazz Hash对象中值的类型。\n\t */\n\t<T> void setHashMap(String key, Map<String, T> map, Class<T> clazz);\n\t\n\t/**\n\t * 获取一个整体的 Map 对象。\n\t * @param key Map对象本身的键\n\t * @param clazz Map对象中值的类型。\n\t * @return Map 对象。\n\t */\n\t<T> Map<String, T> getHashMap(String key, Class<T> clazz) throws BusinessException;\n\n\t/**\n\t * 注册消息机制\n\t * @param channel\n\t * @param message\n\t * @return\n\t * @param <T>\n\t * @throws BusinessException\n\t */\n\t<T> boolean sendMessage(String channel, T message) throws BusinessException;\n\n\n\t<T> Object deserialize(byte[] bytes);\n\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/cache/JedisCacheService.java",
    "content": "package com.terran4j.commons.hedis.cache;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonSyntaxException;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\n\nimport redis.clients.jedis.Jedis;\n\n/**\n * 操作 jedis 不能支持并发访问，如果用 synchronized 同步效率会非常低下。<br>\n * 因此弃用，将使用 {@link RedisTemplateCacheService}，这个里面有连接池的管理，不用做线程同步。\n * @author wei.jiang\n *\n */\n@Deprecated\npublic class JedisCacheService implements CacheService {\n\n\tprivate static Logger log = LoggerFactory.getLogger(JedisCacheService.class);\n\n\tprivate final Jedis jedis;\n\n\tpublic JedisCacheService(Jedis jedis) {\n\t\tsuper();\n\t\tthis.jedis = jedis;\n\t}\n\n\t@Override\n\tpublic boolean existed(String key) {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"请检查传入的key值\");\n\t\t}\n\t\tsynchronized (jedis) {\n\t\t\treturn jedis.exists(key);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void remove(String key) {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"请检查传入的key值\");\n\t\t}\n\t\tsynchronized (jedis) {\n\t\t\tjedis.del(key);\n\t\t}\n\t}\n\n\t@Override\n\tpublic void setObject(String key, Object value, Long expiredMillisecond) {\n\t\tif (StringUtils.isBlank(key) || value == null) {\n\t\t\tthrow new NullPointerException(\"请检查传入参数\");\n\t\t}\n\n\t\tGson g = new Gson();\n\t\tString valueStr = null;\n\t\tString statusCode = null;\n\t\tvalueStr = g.toJson(value);\n\n\t\t// 如果keepTime值为null或keepTime值小于等于0，那么都按永久生效处理\n\t\tif (expiredMillisecond == null || expiredMillisecond <= 0L) {\n\t\t\tsynchronized (jedis) {\n\t\t\t\tstatusCode = jedis.set(key, valueStr);\n\t\t\t}\n\t\t} else {\n\t\t\t// NX|XX, NX -- Only set the key if it does not already exist. XX --\n\t\t\t// Only set the key if it already exist.\n\t\t\t// EX|PX, expire time units: EX = seconds; PX = milliseconds\n\t\t\t// time expire time in the units of <code>expx</code>\n\t\t\tsynchronized (jedis) {\n\t\t\t\tstatusCode = jedis.set(key, valueStr, \"null\", \"px\", expiredMillisecond);\n\t\t\t}\n\t\t}\n\t\tif (log.isDebugEnabled()) {\n\t\t\tlog.debug(\"key=[{}]键保存成功{}，value=[{}]\", key, statusCode, valueStr);\n\t\t}\n\t}\n\n\t@Override\n\tpublic <T> T getObject(String key, Class<T> clazz) throws BusinessException {\n\t\tif (StringUtils.isBlank(key) || clazz == null) {\n\t\t\tthrow new NullPointerException(\"请检查传入参数\");\n\t\t}\n\n\t\tGson g = new Gson();\n\t\tT t = null;\n\t\tString value = null;\n\t\tsynchronized (jedis) {\n\t\t\tvalue = jedis.get(key);\n\t\t}\n\t\tif (!StringUtils.isBlank(value)) {\n\t\t\ttry {\n\t\t\t\tt = (T) g.fromJson(value, clazz);\n\t\t\t} catch (JsonSyntaxException e) {\n\t\t\t\tthrow new BusinessException(CommonErrorCode.JSON_ERROR, e).put(\"methodName\", \"getObject\")\n\t\t\t\t\t\t.put(\"key\", key).put(\"clazz\", clazz).put(\"value\", value)\n\t\t\t\t\t\t.setMessage(\"${methodName}方法, key=${key}, value=${value}, 解析json串失败: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\t\treturn t;\n\t}\n\n\t@Override\n\tpublic void setHashEntry(String key, String entryKey, Object entryValue, Long expiredMillisecond) {\n\t\tif (StringUtils.isBlank(key) || StringUtils.isBlank(entryKey) || entryValue == null) {\n\t\t\tthrow new NullPointerException(\"请检查传入参数\");\n\t\t}\n\n\t\tGson g = new Gson();\n\t\tString valueStr = g.toJson(entryValue);\n\n\t\tsynchronized (jedis) {\n\t\t\tjedis.hset(key, entryKey, valueStr);\n\t\t}\n\n\t\t// 如果keepTime值为null或keepTime值小于等于0，那么都按永久生效处理\n\t\tif (expiredMillisecond != null && expiredMillisecond > 0L) {\n\t\t\tsynchronized (jedis) {\n\t\t\t\tjedis.expire(key, expiredMillisecond.intValue() / 1000);\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic <T> T getHashEntry(String key, String entryKey, Class<T> clazz) throws BusinessException {\n\n\t\tif (StringUtils.isBlank(key) || StringUtils.isBlank(entryKey) || clazz == null) {\n\t\t\tthrow new NullPointerException(\"请检查传入参数\");\n\t\t}\n\n\t\tGson g = new Gson();\n\t\tT t = null;\n\t\tString value = null;\n\t\tsynchronized (jedis) {\n\t\t\tvalue = jedis.hget(key, entryKey);\n\t\t}\n\t\tif (!StringUtils.isBlank(value)) {\n\t\t\ttry {\n\t\t\t\tt = g.fromJson(value, clazz);\n\t\t\t} catch (JsonSyntaxException e) {\n\t\t\t\tthrow new BusinessException(CommonErrorCode.JSON_ERROR, e).put(\"methodName\", \"getHashEntry\")\n\t\t\t\t\t\t.put(\"key\", key).put(\"clazz\", clazz).put(\"value\", value)\n\t\t\t\t\t\t.setMessage(\"${methodName}方法, key=${key}, value=${value}, 解析json串失败: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\t\treturn t;\n\t}\n\n\t@Override\n\tpublic <T> void setHashMap(String key, Map<String, T> map, Class<T> clazz) {\n\t\tif (StringUtils.isBlank(key) || clazz == null) {\n\t\t\tthrow new NullPointerException(\"请检查传入参数\");\n\t\t}\n\n\t\tGson g = new Gson();\n\t\tMap<String, String> tmpMap = new HashMap<String, String>();\n\t\tfor (Map.Entry<String, T> entry : map.entrySet()) {\n\t\t\ttmpMap.put(entry.getKey(), g.toJson(entry.getValue(), clazz));\n\t\t}\n\t\tsynchronized (jedis) {\n\t\t\tjedis.hmset(key, tmpMap);\n\t\t}\n\t}\n\n\t@Override\n\tpublic <T> Map<String, T> getHashMap(String key, Class<T> clazz) throws BusinessException {\n\t\tif (StringUtils.isBlank(key) || clazz == null) {\n\t\t\tthrow new NullPointerException(\"请检查传入参数\");\n\t\t}\n\n\t\tGson g = new Gson();\n\t\tMap<String, T> returnMap = new HashMap<String, T>();\n\t\tMap<String, String> tmpMap = null;\n\t\tsynchronized (jedis) {\n\t\t\ttmpMap = jedis.hgetAll(key);\n\t\t}\n\t\tif (tmpMap != null) {\n\t\t\tfor (Map.Entry<String, String> entry : tmpMap.entrySet()) {\n\t\t\t\ttry {\n\t\t\t\t\treturnMap.put(entry.getKey(), g.fromJson(entry.getValue(), clazz));\n\t\t\t\t} catch (JsonSyntaxException e) {\n\t\t\t\t\tthrow new BusinessException(CommonErrorCode.JSON_ERROR, e).put(\"methodName\", \"getHashMap\")\n\t\t\t\t\t\t\t.put(\"key\", key).put(\"clazz\", clazz).put(\"value\", entry.getValue())\n\t\t\t\t\t\t\t.setMessage(\"${methodName}方法, key=${key}, value=${value}, 解析json串失败: \" + e.getMessage());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn returnMap;\n\t}\n\n\t@Override\n\tpublic <T> boolean sendMessage(String channel, T message) throws BusinessException {\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic <T> T deserialize(byte[] bytes) {\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic boolean setObjectIfAbsent(String key, Object value, Long expiredMillisecond) {\n\t\treturn setObjectNXXX(key, value, expiredMillisecond, \"nx\");\n\t}\n\n\tprivate boolean setObjectNXXX(String key, Object value, Long expiredMillisecond, String nxxx) {\n\t\tif (StringUtils.isBlank(key) || value == null) {\n\t\t\tthrow new NullPointerException(\"请检查传入参数\");\n\t\t}\n\n\t\tGson g = new Gson();\n\t\tString valueStr = null;\n\t\tString statusCode = null;\n\t\tvalueStr = g.toJson(value);\n\n\t\t// NX|XX, NX -- Only set the key if it does not already exist. XX --\n\t\t// Only set the key if it already exist.\n\t\t// EX|PX, expire time units: EX = seconds; PX = milliseconds\n\t\t// time expire time in the units of <code>expx</code>\n\t\tsynchronized (jedis) {\n\t\t\tstatusCode = jedis.set(key, valueStr, nxxx, \"px\", expiredMillisecond);\n\t\t}\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"setObject, key = {}, value = {}, nxxx = {}, expiredMillisecond = {}\", key, value, nxxx,\n\t\t\t\t\texpiredMillisecond);\n\t\t}\n\n\t\treturn \"OK\".equalsIgnoreCase(statusCode);\n\t}\n\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/cache/RedisTemplateCacheService.java",
    "content": "package com.terran4j.commons.hedis.cache;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\nimport com.terran4j.commons.util.Strings;\nimport org.apache.commons.lang3.StringUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.data.redis.core.RedisTemplate;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonSyntaxException;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\n\npublic class RedisTemplateCacheService implements CacheService {\n\n\tprivate static Logger log = LoggerFactory.getLogger(RedisTemplateCacheService.class);\n\t\n\tprivate static final Gson g = new Gson();\n\n\tprivate RedisTemplate<String, String> redisTemplate;\n\t\n\tpublic RedisTemplateCacheService(RedisTemplate<String, String> redisTemplate) {\n\t\tsuper();\n\t\tthis.redisTemplate = redisTemplate;\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"created RedisTemplateCacheService.\");\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean existed(String key) {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is null.\");\n\t\t}\n\t\treturn redisTemplate.opsForValue().getOperations().hasKey(key);\n\t}\n\n\t@Override\n\tpublic void remove(String key) {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is null.\");\n\t\t}\n\t\tredisTemplate.opsForValue().getOperations().delete(key);\n\t}\n\n\t@Override\n\tpublic void setObject(String key, Object value, Long keepTime) {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is null.\");\n\t\t}\n\t\tif (value == null) {\n\t\t\tthrow new NullPointerException(\"value is null.\");\n\t\t}\n\n\t\tString valueText = g.toJson(value);\n\t\t// 如果keepTime值为null或keepTime值小于等于0，那么都按永久生效处理\n\t\tif (keepTime == null || keepTime <= 0L) {\n\t\t\tredisTemplate.opsForValue().set(key, valueText);\n\t\t} else {\n\t\t\tredisTemplate.opsForValue().set(key, valueText, keepTime, TimeUnit.MILLISECONDS);\n\t\t}\n\t}\n\n\t@Override\n\tpublic <T> T getObject(String key, Class<T> clazz) throws BusinessException {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is null\");\n\t\t}\n\t\tif (clazz == null) {\n\t\t\tthrow new NullPointerException(\"clazz is null\");\n\t\t}\n\n\t\tT t = null;\n\t\tString value = redisTemplate.opsForValue().get(key);\n\t\tif (!StringUtils.isBlank(value)) {\n\t\t\ttry {\n\t\t\t\tt = g.fromJson(value, clazz);\n\t\t\t} catch (JsonSyntaxException e) {\n\t\t\t\tthrow new BusinessException(CommonErrorCode.JSON_ERROR, e) //\n\t\t\t\t\t\t.put(\"methodName\", \"getObject\") //\n\t\t\t\t\t\t.put(\"key\", key).put(\"clazz\", clazz).put(\"value\", value) //\n\t\t\t\t\t\t.setMessage(\"${methodName}方法, key=${key}, value=${value}, 解析json串失败: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\t\treturn t;\n\t}\n\n\t@Override\n\tpublic void setHashEntry(String key, String entryKey, Object entryValue, Long keepTime) {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is blank\");\n\t\t}\n\t\tif (StringUtils.isBlank(entryKey)) {\n\t\t\tthrow new NullPointerException(\"entryKey is blank\");\n\t\t}\n\t\tif (entryValue == null) {\n\t\t\tthrow new NullPointerException(\"entryValue is null\");\n\t\t}\n\n\t\tString valueText = g.toJson(entryValue);\n\t\tredisTemplate.opsForHash().put(key, entryKey, valueText);\n\t\t\n\t\t// 如果keepTime值为null或keepTime值小于等于0，那么都按永久生效处理\n\t\tif (keepTime != null && keepTime > 0) {\n\t\t\tredisTemplate.expire(key, keepTime, TimeUnit.MILLISECONDS);\n\t\t}\n\t}\n\n\t@Override\n\tpublic <T> T getHashEntry(String key, String entryKey, Class<T> clazz) throws BusinessException {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is blank\");\n\t\t}\n\t\tif (StringUtils.isBlank(entryKey)) {\n\t\t\tthrow new NullPointerException(\"entryKey is blank\");\n\t\t}\n\t\tif (clazz == null) {\n\t\t\tthrow new NullPointerException(\"clazz is null\");\n\t\t}\n\n\t\tGson g = new Gson();\n\t\tT t = null;\n\t\tString value = (String) redisTemplate.opsForHash().get(key, entryKey);\n\t\tif (!StringUtils.isBlank(value)) {\n\t\t\ttry {\n\t\t\t\tt = g.fromJson(value, clazz);\n\t\t\t} catch (JsonSyntaxException e) {\n\t\t\t\tthrow new BusinessException(CommonErrorCode.JSON_ERROR, e).put(\"methodName\", \"getHashEntry\")\n\t\t\t\t\t\t.put(\"key\", key).put(\"clazz\", clazz).put(\"value\", value)\n\t\t\t\t\t\t.setMessage(\"${methodName}方法, key=${key}, value=${value}, 解析json串失败: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\t\treturn t;\n\t}\n\n\t@Override\n\tpublic <T> void setHashMap(String key, Map<String, T> map, Class<T> clazz) {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is blank\");\n\t\t}\n\t\tif (map == null) {\n\t\t\tthrow new NullPointerException(\"map is null\");\n\t\t}\n\t\tif (clazz == null) {\n\t\t\tthrow new NullPointerException(\"clazz is null\");\n\t\t}\n\n\t\tMap<String, String> temp = new HashMap<String, String>();\n\t\tfor (Map.Entry<String, T> entry : map.entrySet()) {\n\t\t\ttemp.put(entry.getKey(), g.toJson(entry.getValue(), clazz));\n\t\t}\n\t\tredisTemplate.opsForHash().putAll(key, temp);\n\t}\n\n\t@Override\n\tpublic <T> Map<String, T> getHashMap(String key, Class<T> clazz) throws BusinessException {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is blank\");\n\t\t}\n\t\tif (clazz == null) {\n\t\t\tthrow new NullPointerException(\"clazz is null\");\n\t\t}\n\n\t\tMap<String, T> result = new HashMap<String, T>();\n\t\tMap<Object, Object> temp = redisTemplate.opsForHash().entries(key);\n\t\tif (temp == null || temp.size() == 0) {\n\t\t\treturn result;\n\t\t}\n\t\t\n\t\tfor (Map.Entry<Object, Object> entry : temp.entrySet()) {\n\t\t\ttry {\n\t\t\t\tresult.put((String) entry.getKey(), g.fromJson((String) entry.getValue(), clazz));\n\t\t\t} catch (JsonSyntaxException e) {\n\t\t\t\tthrow new BusinessException(CommonErrorCode.JSON_ERROR, e).put(\"methodName\", \"getHashMap\")\n\t\t\t\t\t\t.put(\"key\", key).put(\"clazz\", clazz).put(\"value\", entry.getValue())\n\t\t\t\t\t\t.setMessage(\"${methodName}方法, key=${key}, value=${value}, 解析json串失败: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn result;\n\t}\n\n\t@Override\n\tpublic boolean setObjectIfAbsent(String key, Object value, Long expiredMillisecond) {\n\t\tif (StringUtils.isBlank(key)) {\n\t\t\tthrow new NullPointerException(\"key is blank\");\n\t\t}\n\t\tif (value == null) {\n\t\t\tthrow new NullPointerException(\"value is null\");\n\t\t}\n\t\t\n\t\tString valueText = g.toJson(value);\n\t\tboolean success = redisTemplate.opsForValue().setIfAbsent(key, valueText);\n\t\tif (success && expiredMillisecond != null && expiredMillisecond > 0) {\n\t\t\tredisTemplate.expire(key, expiredMillisecond, TimeUnit.MILLISECONDS);\n\t\t}\n\t\t\n\t\treturn success;\n\t}\n\n\t@Override\n\tpublic <T> boolean sendMessage(String channel, T message) throws BusinessException {\n\t\tif (Strings.isNull(channel)) {\n\t\t\treturn false;\n\t\t}\n\t\ttry {\n\t\t\tredisTemplate.convertAndSend(channel, message);\n\t\t\tlog.info(\"发送消息成功，channel：{}，message：{}\", channel, message);\n\t\t\treturn true;\n\t\t} catch (Exception e) {\n\t\t\tlog.info(\"发送消息失败，channel：{}，message：{}\", channel, message);\n\t\t\te.printStackTrace();\n\t\t}\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic Object deserialize(byte[] bytes) {\n\t\treturn redisTemplate.getValueSerializer().deserialize(bytes);\n\t}\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/config/EnableHedis.java",
    "content": "package com.terran4j.commons.hedis.config;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.springframework.cache.annotation.EnableCaching;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.scheduling.annotation.EnableScheduling;\n\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@EnableCaching\n@EnableScheduling\n@Import(HedisConfiguration.class)\npublic @interface EnableHedis {\n\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/config/HedisConfiguration.java",
    "content": "package com.terran4j.commons.hedis.config;\n\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.PropertyAccessor;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport com.terran4j.commons.hedis.cache.RedisTemplateCacheService;\nimport com.terran4j.commons.hedis.dschedule.DSchedulingAspect;\nimport com.terran4j.commons.hedis.dsyn.DSynchronizedAspect;\nimport com.terran4j.commons.util.Strings;\nimport lombok.extern.slf4j.Slf4j;\nimport org.redisson.Redisson;\nimport org.redisson.api.RedissonClient;\nimport org.redisson.config.Config;\nimport org.redisson.config.SingleServerConfig;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.cache.CacheManager;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.cache.RedisCacheConfiguration;\nimport org.springframework.data.redis.cache.RedisCacheManager;\nimport org.springframework.data.redis.connection.MessageListener;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\nimport org.springframework.data.redis.connection.jedis.JedisConnection;\nimport org.springframework.data.redis.connection.jedis.JedisConnectionFactory;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.data.redis.listener.PatternTopic;\nimport org.springframework.data.redis.listener.RedisMessageListenerContainer;\nimport org.springframework.data.redis.listener.adapter.MessageListenerAdapter;\nimport org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;\nimport org.springframework.util.StringUtils;\nimport redis.clients.jedis.Jedis;\nimport redis.clients.jedis.JedisPoolConfig;\n\nimport java.io.IOException;\nimport java.time.Duration;\n\n@Slf4j\n@Configuration\npublic class HedisConfiguration {\n\n    @Value(\"${spring.redis.host:127.0.0.1}\")\n    private String host;\n\n    @Value(\"${spring.redis.port:6379}\")\n    private int port;\n\n    @Value(\"${spring.redis.password:}\")\n    private String password;\n\n    @Value(\"${spring.redis.pool.max-total:8}\")\n    private int maxTotal;\n\n    @Value(\"${spring.redis.pool.max-idle:8}\")\n    private int maxIdle;\n\n    @Value(\"${spring.redis.pool.min-idle:0}\")\n    private int minIdle;\n\n    /**\n     * 获取连接时的最大等待时间，默认为 -1 表示永久等待。\n     */\n    @Value(\"${spring.redis.pool.max-wait:-1}\")\n    private long maxWait;\n\n    @Value(\"${spring.redis.message.server:}\")\n    private String serverId;\n\n    @Value(\"${spring.redis.message.listener:}\")\n    private String messageListenerClass;\n\n\n    /**\n     * 对于缓存管理器，设置默认的过期时间。\n     */\n    @Value(\"${spring.redis.cache.defaultExpirationSecond:30}\")\n    private int defaultExpiration;\n\n    @Bean\n    public CacheService cacheService(RedisTemplate<String, String> redisTemplate) {\n        return new RedisTemplateCacheService(redisTemplate);\n    }\n\n//    @Bean\n//    public MessageListener getMessageLister(){\n//        if(Strings.isNull(this.messageListenerClass))return null;\n//        try {\n//            return (MessageListener) HedisConfiguration.class.getClassLoader().loadClass(this.messageListenerClass).newInstance();\n//        } catch (ClassNotFoundException e) {\n//            throw new RuntimeException(e);\n//        } catch (InstantiationException e) {\n//            throw new RuntimeException(e);\n//        } catch (IllegalAccessException e) {\n//            throw new RuntimeException(e);\n//        }\n//    }\n\n    @Bean\n    public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory, ApplicationContext context) {\n        RedisMessageListenerContainer container = new RedisMessageListenerContainer();\n        // 监听所有库的key过期事件\n        container.setConnectionFactory(redisConnectionFactory);\n        // 所有的订阅消息，都需要在这里进行注册绑定,new PatternTopic(TOPIC_NAME1)表示发布的主题信息\n        // 可以添加多个 messageListener，配置不同的通道\n        if(!Strings.isNull(this.serverId) && !Strings.isNull(this.messageListenerClass)) {\n            try {\n                MessageListener listener = (MessageListener) context.getBean(HedisConfiguration.class.getClassLoader().loadClass(this.messageListenerClass));\n                log.info(\"rediscontainer.registerlisten {} {}\", this.serverId, listener);\n                container.addMessageListener(listener, new PatternTopic(this.serverId));\n            } catch (ClassNotFoundException e) {\n                throw new RuntimeException(e);\n            }\n//            MessageListener listener = this.getMessageLister();\n        }\n////        container.addMessageListener(adapter, new PatternTopic(TOPIC_NAME2));\n        /**\n         * 设置序列化对象\n         * 特别注意：1. 发布的时候需要设置序列化；订阅方也需要设置序列化\n         *         2. 设置序列化对象必须放在[加入消息监听器]这一步后面，否则会导致接收器接收不到消息\n         */\n        Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Object.class);\n        ObjectMapper objectMapper = new ObjectMapper();\n        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);\n        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);\n        seria.setObjectMapper(objectMapper);\n        container.setTopicSerializer(seria);\n\n        return container;\n    }\n\n//    @Bean\n//    public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {\n//        RedisCacheManager manager = new RedisCacheManager(redisTemplate);\n//        manager.setDefaultExpiration(defaultExpiration); // 设置默认过期时间\n//        return manager;\n//    }\n    @Bean\n    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {\n        Duration expiration = Duration.ofSeconds(defaultExpiration);\n        return RedisCacheManager.builder(redisConnectionFactory)\n                .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(expiration)).build();\n    }\n    @Bean\n    public JedisConnectionFactory jedisConnectionFactory() {\n\n        // 配置Redis连接池\n        JedisPoolConfig config = new JedisPoolConfig();\n\n        config.setMaxTotal(maxTotal);\n\n        // 最大空闲数，不能大于连接池总大小。\n        if (maxIdle > maxTotal) {\n            maxIdle = maxTotal;\n        }\n        config.setMaxIdle(maxIdle);\n\n        // 最小空闲数，不能大于最大空闲数的一半。\n        if (minIdle < 0) {\n            minIdle = 0;\n        }\n        int maxMinIdle = (maxIdle + 1) / 2;\n        if (minIdle > maxMinIdle) {\n            minIdle = maxMinIdle;\n        }\n        config.setMinIdle(minIdle);\n\n        config.setMaxWaitMillis(maxWait);\n\n        JedisConnectionFactory factory = new JedisConnectionFactory();\n        factory.setHostName(host);\n        factory.setPort(port);\n        if (StringUtils.hasText(password)) {\n            factory.setPassword(password.trim());\n        }\n        factory.setPoolConfig(config);\n        if (log.isInfoEnabled()) {\n            log.info(\"Jedis config done: \\n{}\", Strings.toString(config));\n        }\n        return factory;\n    }\n\n    @Bean\n    public Jedis jedis(RedisConnectionFactory factory) {\n        JedisConnection jedisConnection = (JedisConnection) factory.getConnection();\n        Jedis jedis = jedisConnection.getNativeConnection();\n        return jedis;\n    }\n\n    @Bean(destroyMethod = \"shutdown\")\n    public RedissonClient redisson() throws IOException {\n        Config config = new Config();\n        SingleServerConfig serverConfig = config.useSingleServer();\n        serverConfig.setAddress(\"redis://\" + host + \":\" + port);\n        if (StringUtils.hasText(password)) {\n            serverConfig.setPassword(password);\n        }\n        serverConfig.setConnectionPoolSize(maxTotal);\n        serverConfig.setConnectionMinimumIdleSize(minIdle);\n\n        if (log.isInfoEnabled()) {\n            log.info(\"Redisson config done: \\n{}\", Strings.toString(config));\n        }\n        return Redisson.create(config);\n    }\n\n    @Bean\n    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {\n        StringRedisTemplate template = new StringRedisTemplate(factory);\n        Jackson2JsonRedisSerializer<Object> jackson = new Jackson2JsonRedisSerializer<>(Object.class);\n        ObjectMapper objectMapper = new ObjectMapper();\n        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);\n        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);\n        jackson.setObjectMapper(objectMapper);\n        template.setValueSerializer(jackson);\n        template.afterPropertiesSet();\n        return template;\n    }\n\n    @Bean\n    @ConditionalOnMissingBean(DSchedulingAspect.class)\n    public DSchedulingAspect distributedSchedulingAspect() {\n        return new DSchedulingAspect();\n    }\n\n    @Bean\n    @ConditionalOnMissingBean(DSynchronizedAspect.class)\n    public DSynchronizedAspect distributedSynchronizedAspect() {\n        return new DSynchronizedAspect();\n    }\n\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/dschedule/DScheduling.java",
    "content": "package com.terran4j.commons.hedis.dschedule;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.METHOD;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n/**\n * 实现在分布式环境下的任务定时调度。<br>\n * 在 Spring 的<code>@Scheduled</code>进行AOP拦截，\n * 发现有别的实例在执行任务就不执行了。<br>\n * 而在执行任务的实例，会把调度控制标识，通过<code>CacheService</code>\n * 放在分布式缓存中（如：Redis）。<br>\n * <br>\n * <p>\n * 本注解只有写在有<code>@Scheduled</code>注解的类上才有效，有两种写法：<br>\n * 1. 写在有<code>@Scheduled</code>注解的方法上，表示此方法的要在分布式环境下调度。<br>\n * 2. 写在类上，表示此类中所有出现<code>@Scheduled</code>注解的方法都要在分布式环境下调度。<br>\n * <br>\n * <p>\n * TODO: 目前的实现，只考虑到任务在单实例中串行执行的情况，未考虑单实例中多线程并发执行任务的情况。<br>\n *\n * @author wei.jiang\n */\n@Documented\n@Retention(RUNTIME)\n@Target({METHOD})\npublic @interface DScheduling {\n\n    /**\n     * 分布式调度任务的名称，唯一标识。\n     *\n     * @return\n     */\n    String value() default \"\";\n\n    /**\n     * 分布式锁的过期时间，以秒为单位，过了这个时间，锁自动释放。<br>\n     * 请根据此任务的执行时长来设置，一定要比执行时长才行。\n     *\n     * @return\n     */\n    long lockExpiredSecond() default 300;\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/dschedule/DSchedulingAspect.java",
    "content": "package com.terran4j.commons.hedis.dschedule;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.DateTimes;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.aspectj.lang.annotation.Around;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.aspectj.lang.annotation.Pointcut;\nimport org.redisson.api.RLock;\nimport org.redisson.api.RedissonClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.config.EmbeddedValueResolver;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.scheduling.support.CronSequenceGenerator;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.Assert;\nimport org.springframework.util.StringUtils;\nimport org.springframework.util.StringValueResolver;\n\nimport javax.annotation.PostConstruct;\nimport java.lang.reflect.Method;\nimport java.util.Date;\nimport java.util.Map;\nimport java.util.TimeZone;\nimport java.util.UUID;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.TimeUnit;\n\n@Aspect\n@Component\npublic class DSchedulingAspect {\n\n    private static final String instanceId = UUID.randomUUID().toString();\n\n    private static final Map<String, Method> methods = new ConcurrentHashMap<>();\n\n    @Autowired\n    private ConfigurableApplicationContext applicationContext;\n\n    @Autowired\n    private CacheService cacheService;\n\n    @Autowired\n    private RedissonClient redissonClient;\n\n    private EmbeddedValueResolver embeddedValueResolver;\n\n    @PostConstruct\n    public void init() {\n        this.embeddedValueResolver = new EmbeddedValueResolver(\n                applicationContext.getBeanFactory());\n    }\n\n    @Pointcut(\"@annotation(com.terran4j.commons.hedis.dschedule.DScheduling)\")\n    public void distributedScheduling() {\n    }\n\n    @Around(\"distributedScheduling()\")\n    public Object doDistributedScheduling(ProceedingJoinPoint point) throws Throwable {\n\n        // JobExeInfo 用于记录任务信息。\n        JobExeInfo jobExeInfo = new JobExeInfo();\n        long beginTime = System.currentTimeMillis();\n        jobExeInfo.setBeginTime(beginTime);\n        jobExeInfo.setInstanceId(instanceId);\n\n        Object targetObject = point.getTarget();\n        Class<?> targetClass = Classes.getTargetClass(targetObject);\n        String className = targetClass.getName();\n        String methodName = point.getSignature().getName();\n        jobExeInfo.setClassName(className);\n        jobExeInfo.setMethodName(methodName);\n        final Logger log = LoggerFactory.getLogger(targetClass);\n\n        // 只有用 @DScheduling 修饰的方法，才会使用并发控制。\n        Object[] args = point.getArgs();\n        Method method = Classes.getMethod(targetClass, methodName,\n                args, DScheduling.class);\n        if (method == null) {\n            log.error(\"method not found, className = {}, methodName = {}\",\n                    className, methodName);\n            return point.proceed(args);\n        }\n        DScheduling distributedScheduling = method.getAnnotation(DScheduling.class);\n        if (distributedScheduling == null) {\n            log.error(\"@DistributedScheduling not found, className = {},\" +\n                    \" methodName = {}\", className, methodName);\n            return point.proceed(args);\n        }\n\n        // 还得要 @Scheduled 修饰。\n        Scheduled scheduled = method.getAnnotation(Scheduled.class);\n        if (scheduled == null) {\n            String msg = String.format(\"${className}.${methodName}方法\" +\n                            \"上有 %s 注解但没有 %s 注解。\", DScheduling.class,\n                    Scheduled.class);\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n                    .put(\"className\", className)\n                    .put(\"methodName\", methodName)\n                    .setMessage(msg);\n        }\n\n        // 被加上 Scheduled 注解的方法，都是没有参数的，\n        // 因此可以用“类名 + 方法名”唯一标识一个任务。\n        // 但是“类名 + 方法名”很长，\n        // 应用方需要自己设置distributedScheduling 的唯一标识。\n        String value = distributedScheduling.value();\n        if (StringUtils.isEmpty(value)) {\n            value = Classes.toIdentify(method);\n        } else {\n            checkValue(value, method);\n        }\n        if (log.isInfoEnabled()) {\n            log.info(\"start to DistributedScheduling '{}' at: {}\", value,\n                    DateTimes.toString(new Date()));\n        }\n\n        // 计算锁的超时时间。\n        long lockExpiredSecond = distributedScheduling.lockExpiredSecond();\n        if (lockExpiredSecond <= 0) {\n            lockExpiredSecond = 24 * 3600; // 当小于 0 时，设置一个超大的值。\n        }\n        long lockExpired = lockExpiredSecond * 1000;\n        // 获取锁的等待时间较短，长时间等待会阻塞当前线程。\n        long lockWaitTime = Math.max(lockExpired * 2 / 3, 3000);\n        // 任务信息保存时间要比锁要长一些。\n        long jobInfoExpired = lockExpired * 4 / 3;\n\n        // 尝试从 redis 中获取分布式锁，如果没有获取到锁，就不执行本次调度。\n        String jobInfoKey = \"hedis.sj-\" + value;\n        String lockKey = \"hedis.sl-\" + value;\n        RLock lock = redissonClient.getLock(lockKey);\n        boolean locked = lock.tryLock(lockWaitTime, lockExpired, TimeUnit.MILLISECONDS);\n        if (!locked) {\n            // 没有取到锁，不执行任务。\n            if (log.isInfoEnabled()) {\n                JobExeInfo lastInfo = cacheService.getObject(jobInfoKey, JobExeInfo.class);\n                log.info(\"job is executing by other instance, lastInfo:\\n{}\", lastInfo);\n            }\n            return null;\n        } else {\n            log.info(\"get the lock by instance: {}\", instanceId);\n        }\n\n        // 查看上次的执行时间，如果没到这次的执行时间，也应该避免。\n        JobExeInfo lastInfo = cacheService.getObject(jobInfoKey, JobExeInfo.class);\n        if (lastInfo != null) {\n            if (!isValidTime(lastInfo, scheduled, distributedScheduling, log)) {\n                if (log.isInfoEnabled()) {\n                    log.info(\"job is executed by other instance, but next \" +\n                            \"executing not coming, lastInfo: \\n{}\", lastInfo);\n                }\n                lock.unlock();\n                return null;\n            }\n        }\n\n        Object result = null;\n        try {\n            if (log.isInfoEnabled()) {\n                log.info(\"now begin to executing the job.\");\n            }\n            // 执行任务之前，先写入任务的执行信息。\n            jobExeInfo.setRunning(true);\n            cacheService.setObject(jobInfoKey, jobExeInfo, jobInfoExpired);\n\n            // 真正执行任务。\n            result = point.proceed(args);\n            if (log.isInfoEnabled()) {\n                log.info(\"execute job done, args: {}, result: {}\", args, result);\n            }\n\n            jobExeInfo.setResultCode(\"SUCCESS\");\n            if (log.isInfoEnabled()) {\n                log.info(\"executing the job done.\");\n            }\n        } catch (BusinessException e) {\n            jobExeInfo.setResultCode(e.getErrorCode().getName());\n            jobExeInfo.setMessage(e.getMessage());\n            if (log.isInfoEnabled()) {\n                log.info(\"executing the job error: {}\", e.getMessage());\n            }\n        } catch (Throwable e) {\n            jobExeInfo.setResultCode(ErrorCodes.UNKNOWN_ERROR);\n            jobExeInfo.setMessage(e.getMessage());\n            if (log.isInfoEnabled()) {\n                log.info(\"executing the job error: {}\", e.getMessage());\n            }\n        } finally {\n            // 保存本次任务的执行信息。\n            long endTime = System.currentTimeMillis();\n            jobExeInfo.setEndTime(endTime);\n            jobExeInfo.setRunning(false);\n            cacheService.setObject(jobInfoKey, jobExeInfo, jobInfoExpired);\n            if (log.isInfoEnabled()) {\n                log.info(\"done DScheduling, jobExeInfo:\\n{}\", jobExeInfo);\n            }\n\n            // 释放锁。\n            lock.unlock();\n        }\n\n        if (log.isInfoEnabled()) {\n            log.info(\"end of DistributedScheduling '{}' at: {}\", value,\n                    DateTimes.toString(new Date()));\n        }\n        return result;\n    }\n\n    /**\n     * 服务器之间可容忍的时间差距，以毫秒为单位。<br>\n     * 你要尽量保证分布式系统之间的时钟是一致的（这个业界有很多种办法）。<br>\n     * 在保证了时钟一致的情况下，这个值可以尽量的小。<br>\n     *\n     * @return\n     */\n    long tolerableTimeDeviation() {\n        return 10;\n    }\n\n\n    Long getMinPoint(Long a, long b) {\n        if (a == null) {\n            return b;\n        }\n        return a < b ? a : b;\n    }\n\n    boolean isValidTime(JobExeInfo lastInfo, Scheduled scheduled,\n                        DScheduling distributedScheduling, Logger log) {\n        // 之前没有任务执行，视为有效的执行时间。\n        if (lastInfo == null) {\n            return true;\n        }\n\n        // 无效的任务信息，视为无效的执行时间。\n        if (lastInfo.getBeginTime() == null || lastInfo.getEndTime() == null) {\n            if (log.isInfoEnabled()) {\n                log.info(\"Invalid lastInfo(beginTime OR endTime is null): {}\", lastInfo);\n            }\n            return false;\n        }\n\n        long currentTime = System.currentTimeMillis();\n        long lastBeginTime = lastInfo.getBeginTime();\n        long lastEndTime = lastInfo.getEndTime();\n        long tolerableTime = tolerableTimeDeviation();\n        StringValueResolver resolver = getResolver();\n        Long point = null; // 最早开始的时间点。\n\n        String cornText = scheduled.cron();\n        if (StringUtils.hasText(cornText)) {\n            cornText = resolver.resolveStringValue(cornText);\n\n            String zone = scheduled.zone();\n            TimeZone timeZone;\n            if (StringUtils.hasText(zone)) {\n                zone = resolver.resolveStringValue(zone);\n                timeZone = StringUtils.parseTimeZoneString(zone);\n            } else {\n                timeZone = TimeZone.getDefault();\n            }\n            CronSequenceGenerator corn = new CronSequenceGenerator(cornText, timeZone);\n            Long currentPoint = corn.next(new Date(lastEndTime)).getTime();\n            point = getMinPoint(point, currentPoint);\n        }\n\n        long fixedDelay = scheduled.fixedDelay();\n        if (fixedDelay >= 0) {\n            Long currentPoint = lastEndTime + fixedDelay;\n            point = getMinPoint(point, currentPoint);\n        }\n\n        String fixedDelayString = scheduled.fixedDelayString();\n        if (StringUtils.hasText(fixedDelayString)) {\n            fixedDelayString = resolver.resolveStringValue(fixedDelayString);\n            Long currentPoint = lastEndTime + Long.parseLong(fixedDelayString);\n            point = getMinPoint(point, currentPoint);\n        }\n\n        long fixedRate = scheduled.fixedRate();\n        if (fixedRate > 0) {\n            Long currentPoint = lastBeginTime + fixedRate;\n            point = getMinPoint(point, currentPoint);\n        }\n\n        String fixedRateString = scheduled.fixedRateString();\n        if (StringUtils.hasText(fixedRateString)) {\n            fixedRateString = resolver.resolveStringValue(fixedRateString);\n            Long currentPoint = lastBeginTime + fixedRate;\n            point = getMinPoint(point, currentPoint);\n        }\n\n        Assert.isTrue(point != null, \"Invalid scheduled: \" + scheduled);\n\n        return point - tolerableTime <= currentTime;\n    }\n\n    void checkValue(String value, Method method) throws BusinessException {\n        if (method == null) {\n            throw new NullPointerException(\"method is null.\");\n        }\n        if (StringUtils.isEmpty(value)) {\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n                    .put(\"value\", value)\n                    .put(\"className\", method.getDeclaringClass().getName())\n                    .put(\"methodName\", method.getName())\n                    .setMessage(\"@DistributedScheduling value can't be empty.\");\n        }\n        boolean isDuplicate = false;\n        Method existedMethod = methods.get(value);\n        if (existedMethod == null) {\n            synchronized (this) {\n                existedMethod = methods.get(value);\n                if (existedMethod != null && existedMethod != method) {\n                    isDuplicate = true;\n                } else if (existedMethod == null) {\n                    methods.put(value, method);\n                }\n            }\n        } else if (isDuplicate || !existedMethod.equals(method)) {\n            String msg = \"@DistributedScheduling(\\\"${value}\\\") is duplicated with another.\";\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n                    .put(\"value\", value)\n                    .put(\"className\", method.getDeclaringClass().getName())\n                    .put(\"methodName\", method.getName())\n                    .put(\"existedClassName\", existedMethod.getDeclaringClass().getName())\n                    .put(\"existedMethodName\", existedMethod.getName())\n                    .setMessage(msg);\n        }\n    }\n\n    StringValueResolver getResolver() {\n        return this.embeddedValueResolver;\n    }\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/dschedule/JobExeInfo.java",
    "content": "package com.terran4j.commons.hedis.dschedule;\n\nimport com.terran4j.commons.util.Strings;\n\npublic class JobExeInfo {\n\n\tprivate Long beginTime;\n\t\n\tprivate Long endTime;\n\t\n\tprivate String className;\n\t\n\tprivate String methodName;\n\t\n\tprivate String resultCode;\n\t\n\tprivate String message;\n\t\n\tprivate String instanceId;\n\t\n\tprivate boolean running = false;\n\n\t/**\n\t * @return the beginTime\n\t */\n\tpublic final Long getBeginTime() {\n\t\treturn beginTime;\n\t}\n\n\t/**\n\t * @param beginTime the beginTime to set\n\t */\n\tpublic final void setBeginTime(Long beginTime) {\n\t\tthis.beginTime = beginTime;\n\t}\n\n\t/**\n\t * @return the endTime\n\t */\n\tpublic final Long getEndTime() {\n\t\treturn endTime;\n\t}\n\n\t/**\n\t * @param endTime the endTime to set\n\t */\n\tpublic final void setEndTime(Long endTime) {\n\t\tthis.endTime = endTime;\n\t}\n\n\t/**\n\t * @return the resultCode\n\t */\n\tpublic final String getResultCode() {\n\t\treturn resultCode;\n\t}\n\n\t/**\n\t * @param resultCode the resultCode to set\n\t */\n\tpublic final void setResultCode(String resultCode) {\n\t\tthis.resultCode = resultCode;\n\t}\n\n\t/**\n\t * @return the message\n\t */\n\tpublic final String getMessage() {\n\t\treturn message;\n\t}\n\n\t/**\n\t * @param message the message to set\n\t */\n\tpublic final void setMessage(String message) {\n\t\tthis.message = message;\n\t}\n\n\t/**\n\t * @return the instanceId\n\t */\n\tpublic final String getInstanceId() {\n\t\treturn instanceId;\n\t}\n\n\t/**\n\t * @param instanceId the instanceId to set\n\t */\n\tpublic final void setInstanceId(String instanceId) {\n\t\tthis.instanceId = instanceId;\n\t}\n\n\t/**\n\t * @return the className\n\t */\n\tpublic final String getClassName() {\n\t\treturn className;\n\t}\n\n\t/**\n\t * @param className the className to set\n\t */\n\tpublic final void setClassName(String className) {\n\t\tthis.className = className;\n\t}\n\n\t/**\n\t * @return the methodName\n\t */\n\tpublic final String getMethodName() {\n\t\treturn methodName;\n\t}\n\n\t/**\n\t * @param methodName the methodName to set\n\t */\n\tpublic final void setMethodName(String methodName) {\n\t\tthis.methodName = methodName;\n\t}\n\n\t/**\n\t * @return the running\n\t */\n\tpublic final boolean isRunning() {\n\t\treturn running;\n\t}\n\n\t/**\n\t * @param running the running to set\n\t */\n\tpublic final void setRunning(boolean running) {\n\t\tthis.running = running;\n\t}\n\n\tpublic final String toString() {\n\t    return Strings.toString(this);\n    }\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/dschedule/ScheduleContext.java",
    "content": "package com.terran4j.commons.hedis.dschedule;\n\nimport static java.lang.annotation.ElementType.FIELD;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\n/**\n * 标记在非静态的属性上，表示这个属性在调度中维持一个上下文变量。<br>\n * 在执行调度后，此属性的值会保存在持久层（数据库、Redis之类的，看具体实现）。<br>\n * 在执行调度前，从持久层加载上下文数据，并用反射的方式，赋值到此属性上。\n * \n * @author wei.jiang\n */\n@Documented\n@Retention(RUNTIME)\n@Target(FIELD)\npublic @interface ScheduleContext {\n\n\tString value() default \"\";\n\t\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/dsyn/DSynchArgs.java",
    "content": "package com.terran4j.commons.hedis.dsyn;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class DSynchArgs {\n\n\tprivate static final ThreadLocal<Map<String, Object>> buffer = new ThreadLocal<>();\n\t\n\tpublic static final void set(String key, Object value) {\n\t\tMap<String, Object> args = buffer.get();\n\t\tif (args == null) {\n\t\t\targs = new HashMap<>();\n\t\t\tbuffer.set(args);\n\t\t}\n\t\targs.put(key, value);\n\t}\n\t\n\tpublic static final Object get(String key) {\n\t\tMap<String, Object> args = buffer.get();\n\t\tif (args == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn args.get(key);\n\t}\n\t\n\tpublic static final void clear() {\n\t\tbuffer.set(null);\n\t}\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/dsyn/DSynchronized.java",
    "content": "package com.terran4j.commons.hedis.dsyn;\n\nimport static java.lang.annotation.ElementType.METHOD;\nimport static java.lang.annotation.ElementType.TYPE;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\n/**\n * \n * 修饰在方法上，对这个方法在分布式环境下实现同步。<br>\n * \n * <p>\n * 主要是使用分布式锁的原理，根据@DSynchronized(...)中 value 的设置，生成一个锁的key，然后尝试抢这个锁。<br>\n * 在多台实例并发的情况下，同一时刻只有一台实例中的一个线程才能抢到这个锁，并且在执行完后释放这把锁。<br>\n * 没有抢到锁的线程，会进入“等待”状态，直到锁释放时才会被唤醒（即使是被另一台实例的线程释放的）。<br>\n * </p>\n * \n * <p>\n * 分布式锁，可能是 Redis 或 Zookeeper 之类的实现，具体的实现对使用者是透明的。<br>\n * 用于定义锁的 key 的这个 value 非常重要，它决定了并发冲突的范围，如果不设置，则:<br>\n * 锁的 key = 类名 + 方法名 + 所有参数类名拼接起来。<br>\n * 也就意味着所有实例的所有线程，只要执行到这个方法就会竞争同一把锁，相当于方法级别上的同步。<br>\n * </p>\n * \n * 因此，建议精确定义 value 属性，如下所示：<br>\n * \n * <pre class=code>\n * &#64;DSynchronized(\"'increment-' + #name\")\n * public int incrementAndGet(&#64;Param(\"name\") String name) {\n * \t// ...\n * }\n * </pre>\n *\n * \"'increment-' + #name\" 是 Spring 的 EL 表达式，#name 表示从参数 name 中取值。<br>\n * 注意，只会从打了@Param注解的参数中取值。<br>\n * 比如调用方法时参数 name = \"neo\"，则锁的 key 为 \"increment-neo\"，只有遇到同样 key 的线程才会竞争锁。<br>\n * 因此，合理定义 value 属性，可以大大减少竞争锁的情况。\n *\n * @author wei.jiang\n *\n */\n@Documented\n@Retention(RUNTIME)\n@Target({ TYPE, METHOD })\npublic @interface DSynchronized {\n\n\t/**\n\t * 定义锁的key，支持 Spring EL 表达式。\n\t * \n\t * @return 锁的key生成表达式。\n\t */\n\tString value() default \"\";\n\n\t/**\n\t * 竞争锁等待的超时时间，以毫秒为单位。<br>\n\t * 默认为5000毫秒，如果设置小于或等于 0 则表示永不超时。\n\t * \n\t * @return 竞争锁的超时时间。\n\t */\n\tlong timeout() default 5000;\n\n\t/**\n\t * 锁的存活时间，以毫秒为单位，超过这个时间后此锁自动释放。<br>\n\t * 默认为10000毫秒，如果设置小于或等于 0 则表示永不过期。\n\t * \n\t * @return 锁的存活时间。\n\t */\n\tlong keepAlive() default 10000;\n\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/dsyn/DSynchronizedAspect.java",
    "content": "package com.terran4j.commons.hedis.dsyn;\n\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.aspectj.lang.annotation.Around;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.aspectj.lang.annotation.Pointcut;\nimport org.redisson.api.RLock;\nimport org.redisson.api.RedissonClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.repository.query.Param;\nimport org.springframework.expression.EvaluationContext;\nimport org.springframework.expression.Expression;\nimport org.springframework.expression.ExpressionParser;\nimport org.springframework.expression.spel.standard.SpelExpressionParser;\nimport org.springframework.expression.spel.support.StandardEvaluationContext;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\n\nimport javax.annotation.PostConstruct;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.TimeUnit;\n\n@Aspect\n@Component\npublic class DSynchronizedAspect {\n\n    private static Logger log = LoggerFactory.getLogger(DSynchronizedAspect.class);\n\n    private static final ExpressionParser parser = new SpelExpressionParser();\n\n    private static final Map<String, Expression> expressions = new ConcurrentHashMap<>();\n\n    public static final Expression getExpression(String expEL) {\n        Expression exp = expressions.get(expEL);\n        if (exp != null) {\n            return exp;\n        }\n        synchronized (DSynchronizedAspect.class) {\n            exp = expressions.get(expEL);\n            if (exp != null) {\n                return exp;\n            }\n            exp = parser.parseExpression(expEL);\n            expressions.put(expEL, exp);\n            return exp;\n        }\n    }\n\n    @Autowired\n    private RedissonClient redissonClient;\n\n    @PostConstruct\n    public void init() {\n        log.info(\"create DSynchronizedAspect Object.\");\n    }\n\n    @Pointcut(\"@annotation(com.terran4j.commons.hedis.dsyn.DSynchronized)\")\n    public void distributedSynchronized() {\n    }\n\n    @Around(\"distributedSynchronized()\")\n    public Object doDistributedSynchronized(ProceedingJoinPoint point) throws Throwable {\n\n        // 获取当前的要同步的方法。\n        Object targetObject = point.getTarget();\n        Class<?> targetClass = Classes.getTargetClass(targetObject);\n        String className = targetClass.getName();\n        String methodName = point.getSignature().getName();\n        final Logger log = LoggerFactory.getLogger(targetClass);\n        log.info(\"DSynchronized, methodName: {}\", methodName);\n\n        Object[] args = point.getArgs();\n        Method method = Classes.getMethod(targetClass, methodName, args, DSynchronized.class);\n        if (method == null) {\n            log.error(\"method not found, className = {}, methodName = {}\",\n                    className, methodName);\n            return point.proceed(args);\n        }\n        DSynchronized synAnno = method.getAnnotation(DSynchronized.class);\n        if (synAnno == null) {\n            log.error(\"@DSynchronized not found, className = {}, methodName = {}\", className, methodName);\n            return point.proceed(args);\n        }\n\n        // 计算出锁的 key。\n        String keyEL = synAnno.value();\n        String lockKey = getLockKey(keyEL, targetObject, method, args);\n\n        long keepAlive = synAnno.keepAlive();\n        if (keepAlive <= 100) {\n            throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n                    .setMessage(\"keepAlive 设置不能小于 100 毫秒。\")\n                    .put(\"className\", className).put(\"methodName\", methodName)\n                    .put(\"keyEL\", keyEL);\n        }\n\n        long timeout = synAnno.timeout();\n\n        RLock lock = redissonClient.getLock(lockKey);\n        boolean isLocked = false;\n        if (timeout > 0) { // 获取锁有超时时间限制。\n            isLocked = lock.tryLock(timeout, keepAlive, TimeUnit.MILLISECONDS);\n        } else if (timeout <= 0) { // 获取锁时无超时时间限制，获取不到时会一直等待。\n            lock.lock(keepAlive, TimeUnit.MILLISECONDS);\n            isLocked = true;\n        }\n\n        // 获取锁超时了，抛出异常。\n        if (!isLocked) {\n            String msg = String.format(\"DSynchronized, wait lock %s timeout %dms\",\n                    lockKey, timeout);\n            throw new InterruptedException(msg);\n        }\n\n        try {\n            // 获取到锁，执行服务。\n            log.info(\"DSynchronized, lock = {}, keepAlive = {}\", lockKey, keepAlive);\n            return point.proceed();\n        } finally {\n            // 释放锁。\n            log.info(\"DSynchronized, unlock = {}\", lockKey);\n            lock.unlock();\n        }\n    }\n\n    public static String getLockKey(String keyEL, Object target, Method method, Object[] args)\n            throws BusinessException {\n        try {\n            DSynchArgs.clear();\n            return doGetLockKey(keyEL, target, method, args);\n        } finally {\n            DSynchArgs.clear();\n        }\n    }\n\n    static String doGetLockKey(String keyEL, Object target, Method method, Object[] args) throws BusinessException {\n        String lockKey;\n        if (StringUtils.isEmpty(keyEL)) {\n            lockKey = Classes.toIdentify(method);\n        } else {\n            String[] params = getNamesByAnno(method);\n            Expression expression = getExpression(keyEL);\n            EvaluationContext context = new StandardEvaluationContext();\n            context.setVariable(\"target\", target);\n\n            int length = params.length;\n            if (length > 0) {\n                for (int i = 0; i < length; i++) {\n                    String key = params[i];\n                    Object value = args[i];\n                    if (StringUtils.isEmpty(key) || value == null) {\n                        continue;\n                    }\n                    context.setVariable(key, value);\n                    DSynchArgs.set(key, value);\n                }\n            }\n            lockKey = expression.getValue(context, String.class);\n            if (StringUtils.isEmpty(lockKey)) {\n                throw new BusinessException(ErrorCodes.CONFIG_ERROR)\n                        .put(\"keyEL\", keyEL).put(\"keyValue\", lockKey)\n                        .setMessage(\"@DSynchronized中定义锁表达式为空。\");\n            }\n        }\n        return lockKey;\n    }\n\n    public static String[] getNamesByAnno(Method method) {\n        List<String> names = new ArrayList<>();\n        Annotation[][] paramsAnnotations = method.getParameterAnnotations();\n        if (paramsAnnotations != null && paramsAnnotations.length > 0) {\n            for (Annotation[] paramAnnotations : paramsAnnotations) {\n                if (paramAnnotations == null || paramAnnotations.length == 0) {\n                    continue;\n                }\n\n                for (Annotation paramAnno : paramAnnotations) {\n                    if (paramAnno instanceof Param) {\n                        Param param = (Param) paramAnno;\n                        String name = (param == null ? \"\" : param.value());\n                        if (StringUtils.hasText(name)) {\n                            names.add(name);\n                            break;\n                        }\n                    }\n                }\n            }\n        }\n        return names.toArray(new String[names.size()]);\n    }\n\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/commons/hedis/dsyn/Server.java",
    "content": "package com.terran4j.commons.hedis.dsyn;\n\nimport java.util.UUID;\n\npublic class Server {\n\n    private static final String instanceId = UUID.randomUUID().toString();\n\n    public static String getInstanceId() {\n        return instanceId;\n    }\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/mock/hedis/MockCacheService.java",
    "content": "package com.terran4j.mock.hedis;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n@Service\npublic class MockCacheService implements CacheService {\n\n\tprivate final Map<String, Object> cache = new ConcurrentHashMap<>();\n\n\t@Override\n\tpublic boolean existed(String key) {\n\t\treturn cache.containsKey(key);\n\t}\n\n\t@Override\n\tpublic void remove(String key) {\n\t\tcache.remove(key);\n\t}\n\n\t@Override\n\tpublic void setObject(String key, Object value, Long keepTime) {\n\t\tcache.put(key, value);\n\t}\n\t\n\t@Override\n\tpublic synchronized boolean setObjectIfAbsent(String key, Object value, Long keepTime) {\n\t\tif (cache.containsKey(key)) {\n\t\t\treturn false;\n\t\t}\n\t\tsetObject(key, value, keepTime);\n\t\treturn true;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic <T> T getObject(String key, Class<T> clazz) {\n\t\treturn (T) cache.get(key);\n\t}\n\n\t@Override\n\tpublic void setHashEntry(String key, String entryKey, Object entryValue, Long keepTime) {\n\t\tObject value = cache.get(entryKey);\n\t\tif (value instanceof Map) {\n\t\t\t@SuppressWarnings({ \"rawtypes\", \"unchecked\" })\n\t\t\tMap<String, Object> map = (Map<String, Object>) (Map) value;\n\t\t\tmap.put(entryKey, entryValue);\n\t\t} else {\n\t\t\tMap<String, Object> map = new ConcurrentHashMap<>();\n\t\t\tmap.put(entryKey, entryValue);\n\t\t\tcache.put(key, map);\n\t\t}\n\t}\n\n\t@SuppressWarnings({ \"unchecked\", \"rawtypes\" })\n\t@Override\n\tpublic <T> T getHashEntry(String key, String entryKey, Class<T> clazz) {\n\t\tObject value = cache.get(entryKey);\n\t\tif (value instanceof Map) {\n\t\t\treturn (T) ((Map) value).get(entryKey);\n\t\t}\n\t\treturn null;\n\t}\n\n\t@Override\n\tpublic <T> void setHashMap(String key, Map<String, T> map, Class<T> clazz) {\n\t\tcache.put(key, map);\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic <T> Map<String, T> getHashMap(String key, Class<T> clazz) {\n\t\treturn (Map<String, T>) cache.get(key);\n\t}\n\n\t@Override\n\tpublic <T> boolean sendMessage(String channel, T message) throws BusinessException {\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic <T> Object deserialize(byte[] bytes) {\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "commons-hedis/src/main/java/com/terran4j/mock/hedis/MockHedisConfig.java",
    "content": "package com.terran4j.mock.hedis;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class MockHedisConfig {\n\n    public static final MockCacheService cacheService = new MockCacheService();\n\n    @Bean\n    public CacheService cacheService() throws BusinessException {\n        return cacheService;\n    }\n\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/demo/hedis/CountService.java",
    "content": "package com.terran4j.demo.hedis;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport com.terran4j.commons.hedis.dsyn.DSynchronized;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.repository.query.Param;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class CountService {\n\n    @Autowired\n    private CacheService cacheService;\n\n    /**\n     * 一个没有并发控制的递增计算，需要调用方避免并发访问。\n     */\n    public int doIncrementAndGet(String key) {\n        // 从 Redis 缓存中取出计数器变量：\n        Integer counter;\n        try {\n            counter = cacheService.getObject(key, Integer.class);\n            if (counter == null) {\n                counter = 0;\n            }\n        } catch (BusinessException e1) {\n            throw new RuntimeException(e1);\n        }\n\n        // 故意让线程休眠一段时间，让并发问题更严重。\n        try {\n            Thread.sleep(100);\n        } catch (InterruptedException e) {\n            // ignore.\n        }\n\n        // 在本地让计数器变量加 1：\n        counter++;\n\n        // 将变量写回 Redis 缓存：\n        cacheService.setObject(key, counter, null);\n        return counter;\n    }\n\n    /**\n     * 对 incrementAndGet 方法加上分布式并发控制。\n     */\n    @DSynchronized(\"'incrementAndGet-' + #key\")\n    public int incrementAndGet(@Param(\"key\") String key) {\n        return doIncrementAndGet(key);\n    }\n\n}"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/demo/hedis/DSynchronizedCountService.java",
    "content": "package com.terran4j.demo.hedis;\n\nimport java.util.Random;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class DSynchronizedCountService implements ApplicationRunner {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(DSynchronizedCountService.class);\n\n\t@Autowired\n\tprivate CountService countService;\n\n\t@Override\n\tpublic void run(ApplicationArguments args) throws Exception {\n\t\tRandom random = new Random();\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\tint count = countService.doIncrementAndGet(\"count\");\n\t\t\tlog.info(\"\\ncount = {}\", count);\n\n\t\t\tint sleepTime = random.nextInt(50);\n\t\t\ttry {\n\t\t\t\tThread.sleep(sleepTime);\n\t\t\t} catch (InterruptedException e) {\n\t\t\t\t// ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/demo/hedis/DemoCacheService.java",
    "content": "package com.terran4j.demo.hedis;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Date;\n\n@Service\npublic class DemoCacheService implements ApplicationRunner {\n\n    private static final Logger log = LoggerFactory.getLogger(DemoCacheService.class);\n\n    @Autowired\n    private CacheService cacheService;\n\n    @Override\n    public void run(ApplicationArguments args) throws Exception {\n        log.info(\"演示如何使用 CacheService 服务\");\n\n        //  向缓存写入对象。\n        String key = \"u1\";\n        User user = new User(1, \"neo\", new Date());\n        cacheService.setObject(key, user, null);\n\n        // 从 缓存中读取对象。\n        User value = cacheService.getObject(key, User.class);\n        log.info(\"cache key = {}, value = {}\", key, value);\n    }\n\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/demo/hedis/HedisDemoApp.java",
    "content": "package com.terran4j.demo.hedis;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.hedis.config.EnableHedis;\n\n@EnableHedis\n@SpringBootApplication\npublic class HedisDemoApp {\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication app = new SpringApplication(HedisDemoApp.class);\n\t\tapp.setWebEnvironment(false);\n\t\tapp.run(args);\n\t}\n\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/demo/hedis/LoopIncrementJob.java",
    "content": "package com.terran4j.demo.hedis;\n\nimport com.terran4j.commons.hedis.dschedule.DScheduling;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class LoopIncrementJob {\n\n    private static final Logger log = LoggerFactory.getLogger(LoopIncrementJob.class);\n\n    private static final String key = \"demo3-scheduling-counter\";\n\n    @Autowired\n    private CountService countService;\n\n    @DScheduling(lockExpiredSecond = 2)\n    @Scheduled(cron = \"0/1 * * * * *\")\n    public void loopIncrement() {\n        int count = countService.doIncrementAndGet(key);\n        log.info(\"\\nloopIncrement, counter = {}\", count);\n    }\n\n}"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/demo/hedis/User.java",
    "content": "package com.terran4j.demo.hedis;\n\nimport com.terran4j.commons.util.Strings;\n\nimport java.util.Date;\n\npublic class User {\n\n    private long id;\n\n    private String name;\n\n    private Date birthday;\n\n    public User() {\n    }\n\n    public User(long id, String name, Date birthday) {\n        this.id = id;\n        this.name = name;\n        this.birthday = birthday;\n    }\n\n    public long getId() {\n        return id;\n    }\n\n    public void setId(long id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public Date getBirthday() {\n        return birthday;\n    }\n\n    public void setBirthday(Date birthday) {\n        this.birthday = birthday;\n    }\n\n    @Override\n    public String toString() {\n        return Strings.toString(this);\n    }\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/BaseSpringBootTest.java",
    "content": "package com.terran4j.test.hedis;\n\nimport org.junit.runner.RunWith;\nimport org.springframework.test.context.TestExecutionListeners;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.test.context.support.DependencyInjectionTestExecutionListener;\n\n@RunWith(SpringJUnit4ClassRunner.class)\n@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class })\npublic abstract class BaseSpringBootTest {\n\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/BaseTestExecutionListener.java",
    "content": "package com.terran4j.test.hedis;\n\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.TestContext;\nimport org.springframework.test.context.TestExecutionListener;\n\npublic class BaseTestExecutionListener implements TestExecutionListener {\n\n\t@Override\n\tpublic void beforeTestClass(TestContext testContext) throws Exception {\n\t}\n\n\t@Override\n\tpublic void prepareTestInstance(TestContext testContext) throws Exception {\n\t}\n\n\t@Override\n\tpublic void beforeTestMethod(TestContext testContext) throws Exception {\n\t}\n\n\t@Override\n\tpublic void afterTestMethod(TestContext testContext) throws Exception {\n\t}\n\n\t@Override\n\tpublic void afterTestClass(TestContext testContext) throws Exception {\n\t}\n\t\n\tprotected final Class<?>[] getSpringBootClasses(TestContext testContext) {\n\t\tSpringBootTest testAnno = testContext.getTestClass().getAnnotation(SpringBootTest.class);\n\t\tif (testAnno != null) {\n\t\t\tClass<?>[] springBootClasses = testAnno.classes();\n\t\t\tif (springBootClasses != null && springBootClasses.length > 0) {\n\t\t\t\treturn springBootClasses;\n\t\t\t}\n\t\t}\n\t\treturn new Class<?>[0];\n\t}\n\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/CacheAnnoTest.java",
    "content": "package com.terran4j.test.hedis;\n\nimport org.junit.Assert;\nimport org.junit.Test;\n\nimport com.terran4j.test.hedis.dsyn.BaseCacheTest;\nimport com.terran4j.test.hedis.dsyn.Home;\nimport com.terran4j.test.hedis.dsyn.HomeService;\n\npublic class CacheAnnoTest extends BaseCacheTest {\n\t\n\t@Test\n\tpublic void testCacheable() {\n\t\t\n\t\thomeService.create(key);\n\t\t\n\t\tHome home = homeService.get(key);\n\t\tAssert.assertNotNull(home);\n\t\tAssert.assertEquals(1, homeService.getActionTime(HomeService.GET));\n\t\t\n\t\t// 由于 get 方法加了 @Cacheable 注解，后续的请求结果都从缓存中取，而不是直接调用方法。\n\t\tfor (int i = 0; i < 10; i++) {\n\t\t\thome = homeService.get(key);\n\t\t\tAssert.assertNotNull(home);\n\t\t\tAssert.assertEquals(1, homeService.getActionTime(HomeService.GET));\n\t\t}\n\t}\n\t\n\t@Test\n\tpublic void testCachePut() {\n\t\thomeService.create(key);\n\t\t\n\t\tHome home = homeService.get(key);\n\t\tAssert.assertNotNull(home);\n\t\tAssert.assertEquals(1, homeService.getActionTime(HomeService.GET));\n\t\t\n\t\t// 由于 get 方法加了 @Cacheable 注解，后续的请求结果都从缓存中取，而不是直接调用方法。\n\t\thomeService.get(key);\n\t\tAssert.assertEquals(1, homeService.getActionTime(HomeService.GET));\n\t\t\n\t\t// 由于 update 方法加了 @CachePut 注解，会更新缓存数据。\n\t\thomeService.update(key, 5);\n\t\thome = homeService.get(key);\n\t\tAssert.assertEquals(5, home.getMemberCount());\n\t\tAssert.assertEquals(1, homeService.getActionTime(HomeService.GET));\n\t}\n\t\n\t@Test\n\tpublic void testCacheEvict() {\n\t\thomeService.create(key);\n\t\tHome home = homeService.get(key);\n\t\tAssert.assertNotNull(home);\n\t\tAssert.assertEquals(1, homeService.getActionTime(HomeService.GET));\n\t\t\n\t\thomeService.delete(key);\n\t\thomeService.create(key);\n\t\thome = homeService.get(key);\n\t\tAssert.assertNotNull(home);\n\t\tAssert.assertEquals(2, homeService.getActionTime(HomeService.GET));\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/JedisCacheServiceTest.java",
    "content": "package com.terran4j.test.hedis;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.test.hedis.dsyn.BaseCacheTest;\nimport com.terran4j.test.hedis.dsyn.Home;\n\npublic class JedisCacheServiceTest extends BaseCacheTest {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(JedisCacheServiceTest.class);\n\n\tprivate Map<String, Home> createHome() {\n\t\tMap<String, Home> map = new HashMap<String, Home>();\n\t\tHome baba = new Home();\n\t\tbaba.setName(\"ma\");\n\t\tmap.put(\"baba\", baba);\n\t\tHome mama = new Home();\n\t\tmama.setName(\"li\");\n\t\tmap.put(\"mama\", mama);\n\t\tHome girl = new Home();\n\t\tgirl.setName(\"yu\");\n\t\tmap.put(\"girl\", girl);\n\t\treturn map;\n\t}\n\n\t@Test\n\tpublic void testExistedAndRemove() {\n\t\tcacheService.setObject(\"home\", \"testtest\", null);\n\t\tAssert.assertEquals(true, cacheService.existed(\"home\"));\n\t\tcacheService.remove(\"home\");\n\t\tAssert.assertEquals(false, cacheService.existed(\"home\"));\n\t}\n\n\t@Test\n\tpublic void testSetAndGetObject() throws BusinessException {\n\t\tHome home = new Home(\"terran4j\");\n\t\tcacheService.setObject(\"home1\", home, null);\n\t\tAssert.assertEquals(true, cacheService.existed(\"home1\"));\n\t\tHome homeRedis = cacheService.getObject(\"home1\", Home.class);\n\t\tAssert.assertEquals(home, homeRedis);\n\t}\n\n\t@Test\n\tpublic void testGetAndSetHashEntry() throws BusinessException {\n\t\tHome one = new Home(\"one\");\n\t\tcacheService.setHashEntry(\"home2\", \"one\", one, null);\n\t\tAssert.assertEquals(true, cacheService.existed(\"home2\"));\n\t\tHome oneRedis = cacheService.getHashEntry(\"home2\", \"one\", Home.class);\n\t\tAssert.assertEquals(one, oneRedis);\n\t}\n\n\t@Test\n\tpublic void testSetAndGetHashMap() throws BusinessException {\n\t\tMap<String, Home> homes = createHome();\n\t\tcacheService.setHashMap(\"home3\", homes, Home.class);\n\t\tAssert.assertEquals(true, cacheService.existed(\"home3\"));\n\t\tMap<String, Home> homesReis = cacheService.getHashMap(\"home3\", Home.class);\n\t\tAssert.assertEquals(homes, homesReis);\n\t}\n\t\n\t@Test\n\tpublic void testConcurrent() throws Exception {\n\t\tfinal Home one = new Home(\"one\");\n\t\tcacheService.setObject(\"one\", one, null);\n\t\t\n\t\tfinal int threadCount = 10;\n\t\tfinal int exeCount = 10;\n\t\tAtomicBoolean failed = new AtomicBoolean(false);\n\t\tThread[] threads = new Thread[threadCount];\n\t\tlong t0 = System.currentTimeMillis();\n\t\tfor (int i = 0; i < threadCount; i++) {\n\t\t\tthreads[i] = new Thread(new Runnable() {\n\t\t\t\t@Override\n\t\t\t\tpublic void run() {\n\t\t\t\t\tfor (int j = 0; j < exeCount; j++) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tHome obj = cacheService.getObject(\"one\", Home.class);\n\t\t\t\t\t\t\tAssert.assertEquals(one, obj);\n\t\t\t\t\t\t} catch (Throwable e) {\n\t\t\t\t\t\t\te.printStackTrace();\n\t\t\t\t\t\t\tfailed.set(true);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t\tthreads[i].setName(\"Thread - \" + i);\n\t\t\tthreads[i].start();\n\t\t}\n\t\tfor (int i = 0; i < threadCount; i++) {\n\t\t\tthreads[i].join();\n\t\t}\n\t\tlong spend = System.currentTimeMillis() - t0;\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"{} threads, execute {} times per thread, total spend {}ms\",\n\t\t\t\t\tthreadCount, exeCount, spend);\n\t\t}\n\t\tAssert.assertFalse(failed.get());\n\t\tAssert.assertTrue(\"sepnd much more time: \" + spend, spend < 1000);\n\t}\n\n}"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/MockitoInitializer.java",
    "content": "package com.terran4j.test.hedis;\n\nimport org.mockito.MockitoAnnotations;\nimport org.springframework.test.context.TestContext;\n\npublic class MockitoInitializer extends BaseTestExecutionListener {\n\n\t@Override\n\tpublic void prepareTestInstance(TestContext testContext) throws Exception {\n\t\t// 初始化测试用例类中由Mockito的注解标注的所有模拟对象\n\t\tObject instance = testContext.getTestInstance();\n\t\tMockitoAnnotations.initMocks(instance);\n\t}\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/RedisTestConfig.java",
    "content": "package com.terran4j.test.hedis;\n\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class RedisTestConfig {\n\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/RedissonClientTest.java",
    "content": "package com.terran4j.test.hedis;\n\nimport com.terran4j.test.hedis.dsyn.BaseCacheTest;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.redisson.api.RLock;\nimport org.redisson.api.RedissonClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\n\nimport java.util.concurrent.TimeUnit;\n\npublic class RedissonClientTest extends BaseCacheTest {\n\n    private static final Logger log = LoggerFactory.getLogger(RedissonClientTest.class);\n\n    @Autowired\n    private RedissonClient redissonClient;\n\n    private int count = 0;\n\n    private int increment(int i) {\n        try {\n            Thread.sleep(0);\n        } catch (InterruptedException e) {\n            // ignore.\n        }\n        return i + 1;\n    }\n\n    /**\n     * 测试 redissonClient 提供的分布式锁功能。\n     */\n    @Test\n    public void testLock() {\n        RLock lock = redissonClient.getLock(\"my-lock\");\n        count = 0;\n        final int threadCount = 5;\n        final int loopCount = 10;\n        Thread[] threads = new Thread[threadCount];\n        for (int i = 0; i < threadCount; i++) {\n            threads[i] = new Thread(() -> {\n                for (int j = 0; j < loopCount; j++) {\n                    lock.lock(1, TimeUnit.SECONDS);\n                    count = increment(count);\n                    lock.unlock();\n                }\n            });\n            threads[i].start();\n        }\n        for (int i = 0; i < threadCount; i++) {\n            try {\n                threads[i].join();\n            } catch (InterruptedException e) {\n                Assert.fail(\"InterruptedException: \" + e.getMessage());\n            }\n        }\n        Assert.assertEquals(threadCount * loopCount, count);\n    }\n\n}"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/SchedulingApplication.java",
    "content": "package com.terran4j.test.hedis;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.scheduling.annotation.Scheduled;\n\nimport com.terran4j.commons.hedis.config.EnableHedis;\nimport com.terran4j.commons.hedis.dschedule.DScheduling;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.test.hedis.dsyn.CountService;\n\n@EnableHedis // 一定要加这个，不然不会启动任务调度。\n@SpringBootApplication\npublic class SchedulingApplication {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(SchedulingApplication.class);\n\t\n\t@Autowired\n\tprivate CountService countService;\n\n\t@DScheduling(\"SchedulingDemo\")\n\t@Scheduled(cron = \"0/${service.scheduling.runRate:5} * * * * *\")\n\tpublic void exe() throws InterruptedException {\n\t\ttry {\n\t\t\tlog.info(\"\\nexe, time = \" + countService.incrementAndGet());\n\t\t} catch (BusinessException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t}\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication app = new SpringApplication(SchedulingApplication.class);\n\t\tapp.setWebEnvironment(false);\n\t\tapp.run(args);\n\t}\n\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/dsyn/BaseCacheTest.java",
    "content": "package com.terran4j.test.hedis.dsyn;\n\nimport com.terran4j.test.hedis.BaseSpringBootTest;\nimport com.terran4j.test.hedis.MockitoInitializer;\nimport org.apache.commons.lang3.StringUtils;\nimport org.junit.After;\nimport org.junit.Before;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.SpringBootTest.WebEnvironment;\nimport org.springframework.test.context.TestExecutionListeners;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\n\n@SpringBootTest(classes = { CacheTestApplication.class }, webEnvironment = WebEnvironment.NONE)\n@TestExecutionListeners({ MockitoInitializer.class })\npublic abstract class BaseCacheTest extends BaseSpringBootTest {\n\n\tprivate static Logger log = LoggerFactory.getLogger(BaseCacheTest.class);\n\n\t@Autowired\n\tprotected CacheService cacheService;\n\t\n\t@Autowired\n\tprotected HomeService homeService;\n\t\n\t@Autowired\n\tprotected DSynchronizedService dSynchronizedService;\n\t\n\tprotected final String key = \"terran4j\";\n\n\t@Before\n\tpublic void setUp() {\n\t\tcacheService.remove(\"home-\" + key);\n\t\thomeService.clear();\n\t\tlog.info(StringUtils.center(\" test start \", 30, \"=\"));\n\t}\n\n\t@After\n\tpublic void tearDown() {\n\t\tlog.info(StringUtils.center(\" test end \", 30, \"=\"));\n\t}\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/dsyn/CacheTestApplication.java",
    "content": "package com.terran4j.test.hedis.dsyn;\n\nimport com.terran4j.test.hedis.RedisTestConfig;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Import;\n\nimport com.terran4j.commons.hedis.config.EnableHedis;\n\n@EnableHedis\n@Import({ RedisTestConfig.class })\n@SpringBootApplication\npublic class CacheTestApplication {\n\n\t@Bean\n\tpublic HomeService homeService() {\n\t\treturn new HomeService();\n\t}\n\n}"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/dsyn/CountService.java",
    "content": "package com.terran4j.test.hedis.dsyn;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport com.terran4j.commons.util.error.BusinessException;\n\n@Service\npublic class CountService {\n\t\n\tprivate static final String key = \"test_scheduling_counter\";\n\t\n\t@Autowired\n\tprivate CacheService cacheService;\n\t\n\t/**\n\t * 一个没有并发控制的递增计算，需要调用方避免并发访问。\n\t * @throws BusinessException\n\t */\n\tpublic int incrementAndGet() throws BusinessException {\n\t\tInteger value = cacheService.getObject(key, Integer.class);\n\t\tif (value == null) {\n\t\t\tvalue = 0;\n\t\t}\n\t\tvalue++;\n\t\tcacheService.setObject(key, value, null);\n\t\treturn value;\n\t}\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/dsyn/DSynchronizedService.java",
    "content": "package com.terran4j.test.hedis.dsyn;\n\nimport java.util.Random;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.repository.query.Param;\nimport org.springframework.stereotype.Service;\n\nimport com.terran4j.commons.hedis.cache.CacheService;\nimport com.terran4j.commons.hedis.dsyn.DSynchronized;\nimport com.terran4j.commons.util.error.BusinessException;\n\n@Service\npublic class DSynchronizedService {\n\n\tprivate static final Random random = new Random();\n\n\t@Autowired\n\tprivate CacheService cacheService;\n\n\t/**\n\t * 一个需要并发控制的“++”计算。\n\t * \n\t * @param name\n\t * @param spendTime1\n\t * @param spendTime2\n\t * @return\n\t * @throws BusinessException\n\t */\n\t@DSynchronized(value = \"'increment_' + #name\")\n\tpublic int incrementAndGet(@Param(\"name\") String name, long spendTime1, long spendTime2) throws BusinessException {\n\t\tInteger current = cacheService.getObject(name, Integer.class);\n\t\tif (current == null) {\n\t\t\tcurrent = new Integer(0);\n\t\t\tcacheService.setObject(name, current, null);\n\t\t}\n\n\t\tsleep(spendTime1);\n\n\t\tint result = current + 1;\n\t\tcacheService.setObject(name, new Integer(result), null);\n\n\t\tsleep(spendTime2);\n\n\t\treturn getValue(name);\n\t}\n\n\tpublic int getValue(String name) throws BusinessException {\n\t\tInteger result = cacheService.getObject(name, Integer.class);\n\t\treturn result == null ? 0 : result;\n\t}\n\n\tprivate void sleep(long sleepTime) {\n\t\tif (sleepTime < 0) {\n\t\t\tint max = 0 - (int) sleepTime;\n\t\t\tint actualSleepTime = random.nextInt(max);\n\t\t\ttry {\n\t\t\t\tThread.sleep(actualSleepTime);\n\t\t\t} catch (InterruptedException e) {\n\t\t\t}\n\t\t} else if (sleepTime > 0) {\n\t\t\ttry {\n\t\t\t\tThread.sleep(sleepTime);\n\t\t\t} catch (InterruptedException e) {\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/dsyn/DSynchronizedTest.java",
    "content": "package com.terran4j.test.hedis.dsyn;\n\nimport com.terran4j.commons.hedis.dsyn.DSynchArgs;\nimport com.terran4j.commons.hedis.dsyn.DSynchronizedAspect;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.data.repository.query.Param;\nimport org.springframework.util.ReflectionUtils;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class DSynchronizedTest extends BaseCacheTest {\n\n    private static final Logger log = LoggerFactory.getLogger(DSynchronizedTest.class);\n\n    private final String key = \"k1\";\n\n    @Before\n    public void setUp() {\n        super.setUp();\n        cacheService.remove(key);\n        cacheService.remove(\"increment-k1\");\n    }\n\n    public String toKey() {\n        int a = (int) DSynchArgs.get(\"a\");\n        int b = (int) DSynchArgs.get(\"b\");\n        if (a < b) {\n            return a + \"-\" + b;\n        } else {\n            return b + \"-\" + a;\n        }\n    }\n\n    public String toKey(int a, int b) {\n        if (a < b) {\n            return a + \"-\" + b;\n        } else {\n            return b + \"-\" + a;\n        }\n    }\n\n    public void doSomething(@Param(\"a\") int a, @Param(\"b\") int b) {\n    }\n\n    @Test\n    public void testGetLockKeyWithArgs() {\n        Method method = ReflectionUtils.findMethod(getClass(), \"doSomething\",\n                int.class, int.class);\n        Assert.assertNotNull(method);\n        String keyEL = \"#target.toKey(#a, #b)\";\n        String key = null;\n        try {\n            key = DSynchronizedAspect.getLockKey(keyEL, this, method, new Object[]{1, 6});\n            Assert.assertEquals(\"1-6\", key);\n            key = DSynchronizedAspect.getLockKey(keyEL, this, method, new Object[]{3, 2});\n            Assert.assertEquals(\"2-3\", key);\n        } catch (BusinessException e) {\n            e.printStackTrace();\n            Assert.fail(e.getMessage());\n        }\n    }\n\n    @Test\n    public void testGetLockKey() {\n        Method method = ReflectionUtils.findMethod(getClass(), \"doSomething\",\n                int.class, int.class);\n        Assert.assertNotNull(method);\n        String keyEL = \"#target.toKey()\";\n        String key = null;\n        try {\n            key = DSynchronizedAspect.getLockKey(keyEL, this, method, new Object[]{1, 9});\n            Assert.assertEquals(\"1-9\", key);\n            key = DSynchronizedAspect.getLockKey(keyEL, this, method, new Object[]{3, 2});\n            Assert.assertEquals(\"2-3\", key);\n        } catch (BusinessException e) {\n            e.printStackTrace();\n            Assert.fail(e.getMessage());\n        }\n    }\n\n    @Test\n    public void testDSynchronized() throws Exception {\n        int result = dSynchronizedService.incrementAndGet(key, 0, 0);\n        Assert.assertEquals(1, result);\n        dSynchronizedService.incrementAndGet(key, 0, 0);\n        result = dSynchronizedService.getValue(key);\n        Assert.assertEquals(2, result);\n    }\n\n    @Test\n    public void testDSynchronizedMultiThread() throws Exception {\n        final int threadCount = 5;\n        final int exeCount = 10;\n        final List<Throwable> failed = new ArrayList<>();\n        Thread[] threads = new Thread[threadCount];\n        long t0 = System.currentTimeMillis();\n        for (int i = 0; i < threadCount; i++) {\n            threads[i] = new Thread(() -> {\n                for (int j = 0; j < exeCount; j++) {\n                    try {\n                        dSynchronizedService.incrementAndGet(\n                                key, -1, -1);\n                    } catch (Throwable e) {\n                        failed.add(e);\n                    }\n                }\n            });\n            threads[i].setName(\"Thread - \" + i);\n            threads[i].start();\n        }\n        for (int i = 0; i < threadCount; i++) {\n            threads[i].join();\n        }\n        long spend = System.currentTimeMillis() - t0;\n        if (log.isInfoEnabled()) {\n            log.info(\"{} threads, execute {} times per thread, total spend {}ms\",\n                    threadCount, exeCount, spend);\n        }\n        for (Throwable fail : failed) {\n            fail.printStackTrace();\n        }\n        Assert.assertFalse(failed.size() > 0);\n        Assert.assertEquals(threadCount * exeCount,\n                dSynchronizedService.getValue(key));\n    }\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/dsyn/Home.java",
    "content": "package com.terran4j.test.hedis.dsyn;\n\npublic class Home {\n\n\tprivate String name;\n\t\n\tprivate int memberCount = 1;\n\t\n\tpublic Home() {\n\t\tsuper();\n\t}\n\n\tpublic Home(String name) {\n\t\tsuper();\n\t\tthis.name = name;\n\t}\n\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\tpublic final int getMemberCount() {\n\t\treturn memberCount;\n\t}\n\n\tpublic final void setMemberCount(int memberCount) {\n\t\tthis.memberCount = memberCount;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"Home [name=\" + name + \", memberCount=\" + memberCount + \"]\";\n\t}\n\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#hashCode()\n\t */\n\t@Override\n\tpublic int hashCode() {\n\t\tfinal int prime = 31;\n\t\tint result = 1;\n\t\tresult = prime * result + memberCount;\n\t\tresult = prime * result + ((name == null) ? 0 : name.hashCode());\n\t\treturn result;\n\t}\n\n\t/* (non-Javadoc)\n\t * @see java.lang.Object#equals(java.lang.Object)\n\t */\n\t@Override\n\tpublic boolean equals(Object obj) {\n\t\tif (this == obj)\n\t\t\treturn true;\n\t\tif (obj == null)\n\t\t\treturn false;\n\t\tif (getClass() != obj.getClass())\n\t\t\treturn false;\n\t\tHome other = (Home) obj;\n\t\tif (memberCount != other.memberCount)\n\t\t\treturn false;\n\t\tif (name == null) {\n\t\t\tif (other.name != null)\n\t\t\t\treturn false;\n\t\t} else if (!name.equals(other.name))\n\t\t\treturn false;\n\t\treturn true;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-hedis/src/test/java/com/terran4j/test/hedis/dsyn/HomeService.java",
    "content": "package com.terran4j.test.hedis.dsyn;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport org.springframework.cache.annotation.CacheConfig;\nimport org.springframework.cache.annotation.CacheEvict;\nimport org.springframework.cache.annotation.CachePut;\nimport org.springframework.cache.annotation.Cacheable;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\n\n/**\n * 本类模拟了对 Home 实体进行增删查改的操作。<br>\n * 同时演示了如何用 @CachePut, @CacheEvict, @Cacheable 注解来管理缓存数据。\n * \n * @author wei.jiang\n *\n */\n@CacheConfig(cacheNames = \"homes\")\n@Service\npublic class HomeService {\n\n\tpublic static final String DELETE = \"delete\";\n\n\tpublic static final String GET = \"get\";\n\n\tpublic static final String UPDATE = \"update\";\n\n\tpublic static final String CREATE = \"create\";\n\n\tprivate Map<String, Home> homes = new ConcurrentHashMap<>();\n\n\tprivate Map<String, AtomicInteger> actionTimes = new ConcurrentHashMap<>();\n\n\tpublic HomeService() {\n\t\tclear();\n\t}\n\n\tpublic synchronized int getActionTime(String action) {\n\t\treturn actionTimes.get(action).get();\n\t}\n\n\tpublic void clear() {\n\t\thomes = new ConcurrentHashMap<>();\n\t\tactionTimes = new ConcurrentHashMap<>();\n\t\tactionTimes.put(CREATE, new AtomicInteger(0));\n\t\tactionTimes.put(UPDATE, new AtomicInteger(0));\n\t\tactionTimes.put(GET, new AtomicInteger(0));\n\t\tactionTimes.put(DELETE, new AtomicInteger(0));\n\t}\n\n\tpublic Home create(String name) {\n\t\tactionTimes.get(CREATE).incrementAndGet();\n\t\tif (homes.containsKey(name)) {\n\t\t\tthrow new RuntimeException(\"home existed: \" + name);\n\t\t}\n\t\tHome home = new Home(name);\n\t\thomes.put(name, home);\n\t\treturn home;\n\t}\n\n\t/**\n\t * <code>@Cacheable</code>: 配置于函数上，能够缓存函数调用的返回值。<br>\n\t * 在调用函数之前，会先从缓存中获取，若不存在才真正调用函数，并缓存函数的返回值。<br>\n\t * 所以 Cacheable 注解一般用在查询操作上。\n\t * \n\t * @param name\n\t * @return\n\t */\n\t@Cacheable(key = \"'home-' + #name\")\n\tpublic Home get(String name) {\n\t\tactionTimes.get(GET).incrementAndGet();\n\t\tif (StringUtils.isEmpty(name)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn homes.get(name);\n\t}\n\n\t/**\n\t * <code>@CachePut</code>: 配置于函数上，能够缓存函数调用的结果。<br>\n\t * 它与@Cacheable不同的是，它每次都会先真的调用函数，再缓存函数的返回值。 <br>\n\t * 所以 CachePut 主要用于数据新增和修改操作上。\n\t * \n\t * @param name\n\t * @param memberCount\n\t * @return\n\t */\n\t@CachePut(key = \"'home-' + #name\")\n\tpublic Home update(String name, int memberCount) {\n\t\tactionTimes.get(UPDATE).incrementAndGet();\n\t\tif (!homes.containsKey(name)) {\n\t\t\tthrow new RuntimeException(\"home not exist: \" + name);\n\t\t}\n\t\tHome home = homes.get(name);\n\t\thome.setMemberCount(memberCount);\n\t\treturn home;\n\t}\n\n\t/**\n\t * <code>@CacheEvict</code>: 配置于函数上，删除对应的缓存。<br>\n\t * 所以 CacheEvict 一般用在删除操作上，用来从缓存中删除相应数据。\n\t * \n\t * @param name\n\t * @return\n\t */\n\t@CacheEvict(key = \"'home-' + #name\")\n\tpublic Home delete(String name) {\n\t\tactionTimes.get(DELETE).incrementAndGet();\n\t\treturn homes.remove(name);\n\t}\n\n}\n"
  },
  {
    "path": "commons-hi/.gitignore",
    "content": "/target/\n/.classpath\n/.project\n/.settings/\n*.iml"
  },
  {
    "path": "commons-hi/README.md",
    "content": "\n### 背景\n\n在互联网繁荣的今天，HTTP + JSON 形式的 API 非常流行（即请求是 HTTP/HTTPS 协议，返回数据是 json 格式)。\n\n然而调用 HTTP API 是一件比较繁琐的事情，虽然有很多开源工具帮助我们简化了 HTTP 调用的一些底层细节（如：HttpClient），但业务层面上，仍有需要编程来处理一些繁琐的细节。\n\n比如我们看一下以下问题：\n\n* HTTP调用时，有很多属性需要设置，如：\n\n&emsp;&emsp; a. 设置方法，有：GET、POST、PUT...等多种方法;\n\n&emsp;&emsp; b. 设置参数，并且对于GET方法，参数要拼在URL后面；对于POST方法，参数要URLEncode后拼起来放在body中;\n\n&emsp;&emsp; c. 设置Http Header属性;\n\n&emsp;&emsp; d. 设置Http Cookie属性;\n\n  在调用HTTP API时，这些细节的实现都耦合在代码中，使代码变得臃肿，如果将来 API 有调整了，就需要修改代码以适应 API 的变化，不易于维护。\n\n* 很多 HTTP 服务是无状态的（因为这样对服务器来说比较容易水平扩展），那这会话状态就需要客户端来维持，比如在很多应用中会话状态是这样维护的： \n\n&emsp;&emsp;  a. 首先客户端调用一个`/login`的登录请求，服务器会返回一个`access_token`的字段，作为登录成功后的凭证。\n\n&emsp;&emsp;  b. 然后客户端需要保存这个`access_token`字段，以后每次访问其它请求时，都需求将这个`access_token`放在`Http Header或`Http Cookie`中提交。\n\n&emsp;&emsp;  c. 服务端会校验这个`access_token`，并依此获取当前用户是否登录及用户信息，这样就实现了用户会话状态的维护。\n\n&emsp;&emsp;  这类方式对服务器端是简化了，但增加了客户端的工作量，我们在调用 HTTP 服务时，需要编写一些额外的代码实现会话状态的维护及相关细节，这一定程序上增加了代码的复杂性。\n\n\n\n### 解决方案\n\ncommons-hi （下面简称为 hi）提供了一种配置化的方式，\n使对 HTTP API 调用更加简单，并使代码更易于理解。\n\n下一节，我们将用一个示例程序来体验一下它是如何工作的。\n\n\n### 示例程序\n\n假设我们要开发一个“累加计算”的应用程序，需求是这样的：\n1. 打开应用时，初始值（后面称为total）为0.\n2. 用户可以在客户端输入一个数字n，点击提交，让服务器计算 total + n 的和，计算结果 result 返回给客户端。\n3. 客户端将 result 值赋值给 total，以保存累加计算的值。\n4. 可以不停执行 2 ~ 3 步，以实现“累加计算”。\n\n这个程序有点无聊，但它足够简单，通过它能快速演示 hi 提供的功能。\n\n##### 服务端实现\n\n首选，我们实现服务端代码，我们提供一个求和计算的HTTP API，实现两个数字求和计算。\n这个 API 的 url 为： `http://${server}/calculator/plus`, 接受 GET 请求，入参为 a, b 返回数据为：\n```\n{\n    \"a\": 1,\n    \"b\": 2,\n    \"result\": 3\n}\n```\na, b 表示输入的两个参数值， result 是 a + b 的结果值。\n注意：为了实现服务端的无状态性，它并没有在 session 中保存 result 值。\n\n我们编写一个类实现求和计算：\n```java\n\npublic class Calculator {\n\n\tprivate final long a;\n\t\n\tprivate final long b;\n\t\n\tprivate long result;\n\n\tpublic Calculator(long a, long b) {\n\t\tsuper();\n\t\tthis.a = a;\n\t\tthis.b = b;\n\t}\n\t\n\tpublic long getA() {\n\t\treturn a;\n\t}\n\n\tpublic long getB() {\n\t\treturn b;\n\t}\n\n\tpublic long getResult() {\n\t\treturn result;\n\t}\n\n\tpublic Calculator plus() {\n\t\tresult = a + b;\n\t\treturn this;\n\t}\n}\n```\n\n然后，我们基于 Spring Boot 编写一个 Controller 来提供一个 HTTP API，代码如下：\n```java\n@RequestMapping(\"/calculator\")\n@RestController\npublic class CalculatorController {\n\n\t@RequestMapping(\"/plus\")\n\t@ResponseBody\n\tpublic Calculator plus(@RequestParam(\"a\") long a, @RequestParam(\"b\") long b) {\n\t\treturn new Calculator(a, b).plus();\n\t}\n\t\n}\n```\n\n这里，需要读者有一点 Spring Boot 的编程经验，如果您完全不了解 Spring Boot ，请先学习下 Spring Boot 。\n\n最后，我们启动 Spring Boot 应用程序，在浏览器上访问： `http://localhost:8080/calculator/plus?a=3&b=5`，\n结果返回： `{\"a\":3,\"b\":5,\"result\":8}`， 表示执行成功。\n\n\n##### 客户端实现\n\n现在，我们的重点来了，我们将用 hi 实现客户端功能。\n传统的方式，是用 HttpConnection 或 HttpClient 之类的工具类，直接硬编码实现对 Http 接口的调用，\n而在 hi 中，我们将先编写一个`http.config.json`文件来定义对这个 HTTP 接口的调用细节：\n\n```json\n{\n\t\"locals\": {\n\t\t\"total\": \"0\"\n\t},\n\t\"actions\": [\n\t\t{\n\t\t\t\"id\": \"plus\",\n\t\t\t\"name\": \"两数相加\",\n\t\t\t\"url\": \"${url}/calculator/plus\",\n\t\t\t\"method\": \"POST\",\n\t\t\t\"params\": {\n\t\t\t\t\"a\": \"${total}\",\n\t\t\t\t\"b\": \"${input}\"\n\t\t\t},\n\t\t\t\"writes\": [\n\t\t\t\t{ \"to\": \"locals\", \"key\": \"total\", \"value\": \"${result}\" }\n\t\t\t]\n\t\t}\n\t]\n}\n```\n\n说明一下：\nlocals:  定义了客户端要维护的本地变量，上面定义了一个名为`total`本地变量，初始值为0。\nactions: 定义了所有的 HTTP 接口，如上面定义了一个 id 为 plus 的 HTTP 接口（即`action`），这个`action`的 id 将在编程时用到，如：\n\n&emsp;&emsp;  `session.action(\"plus\").param(\"input\", \"3\").exe()` \n\n表示调用了这个 plus 接口，并传入了一个名为input的入参。\n\n这个 plus 接口，按 json 文件中的描述是这样的：\n\n`\"url\": \"${url}/calculator/plus\"` 表示这个 HTTP 接口的 url 为`${url}/calculator/plus`，注意: json 定义中的值，可以用 ${...} 来引用一个变量，变量值从以下几个地方读取：\n\n* Spring 容器中的环境配置，比如上面的这个 ${url} 是在`application.yml`文件中定义的：\n\n```yml\nurl:  http://localhost:8080\n```\n\n它完全遵循 Spring Boot 的原理，指定了不同的环境，可以加载对应这个环境的配置值。\n\n*  locals 中的值，也就是 hi 给当前会话提供的本地变量，这个本地变量可能会被改写。\n\n* 调用 action 时的入参，如`session.action(\"plus\").param(\"input\", \"3\").exe()`这次调用，指定了一个入参 input = 3。\n\n以上变量加载的优先级为： action入参 > locals变量 > Spring环境配置。\n\n`\"method\": \"POST\"` 表示发起 POST 请求。\n\n`\n\"params\": {\n    \"a\": \"${total}\",\n    \"b\": \"${input}\"\n}\n`\n表示 HTTP 请求要提交两个参数：参数 a 的值为 ${total}，即从 locals 变量中取 total 的值；参数 b 的值为 ${input}，即从action入参中取 input 参数的值。\n\n`\n\"writes\": [\n    { \"to\": \"locals\", \"key\": \"total\", \"value\": \"${result}\" }\n]\n`\n指示在执行完 HTTP 请求后，要将返回结果写入到一些地方保存起来：\n* `\"to\"` 表示写入的地方，有三个值可选：`locals`表示要写入到本地变量中，后续的请求可以用${...}的方式从本地变量中取到这个值；`headers`表示要写入到 Http Header 中，后续的请求的 HTTP Header 中都会带上这个字段； `cookies`表示要写入到 Http Cookie 中，后续的请求都的 HTTP Cookie 中都会带上这个字段。\n* `\"key\"` 表示要写入字段的 key 。\n* `\"value\"` 表示要写入字段的值，这里可以用 ${...} 引用本次请求中返回数据中的值，可以用.号分隔的路径表达式引用具有嵌套结构的数据，比如返回数据为：\n```\n{\n    \"data\": {\n        \"user\": {\n            \"name\": \"neo4j\",\n            \"title\": \"CEO\",\n            \"age\": 28\n        }\n    }\n}\n```\n那`${data.user.name}`引用的值即为`neo4j`。\n\nhttp.config.json 文件放在 classpth 的根路径即可。\n\n在编写完`http.config.json`文件之后，调用 HTTP API 的 java 代码就特别简单了，如下所示：\n\n```java\n@RunWith(SpringJUnit4ClassRunner.class)\n@SpringBootTest(classes = { HttpClientApp.class }, webEnvironment = WebEnvironment.DEFINED_PORT)\npublic class HttpClientTest {\n    \n\t@Autowired\n\tprotected ApplicationContext context;\n\t\n\t@Test\n\tpublic void testHttpClient() throws HttpException {\n\t\t\n\t\t// 创建一个 httpClient 对象，它会加载本地 http.config.json 文件中定义的信息。\n\t\tHttpClient httpClient = HttpClient.create(context);\n\t\t\n\t\t// 创建一个 session 对象，它会在客户端维护一些会话信息，如 locals 变量、cookies 变量之类的。\n\t\tSession session = httpClient.create();\n\t\t\n\t\t// 调用 plus 接口，指定入参 input = 3，exe()方法会真正调用 HTTP 请求。\n\t\tResponse response = session.action(\"plus\").param(\"input\", \"3\").exe();\n\t\t\n\t\t// 从返回结果中，取 result 字段的值。\n\t\tint total = response.getJson(\"result\").getAsInt();\n\t\tAssert.assertEquals(3, total);\n\t\t\n\t\t// 再调用一次，发现返回结果确实在累加。\n\t\tresponse = session.action(\"plus\").param(\"input\", \"5\").exe();\n\t\ttotal = response.getJson(\"result\").getAsInt();\n\t\tAssert.assertEquals(8, total);\n\t}\n\t\n}\n```\n\n上面的代码很好理解吧 :-)\n\n\n### 总结\n\nhi 的好处是对 HTTP 的调用细节从代码提取到配置文件中描述，\n让真正调用 HTTP 接口时特别简单、容易理解，从而提高了代码的可维护性。\n\n笔者目前用它最多的场景是编写 HTTP 接口的 Test Case 代码。"
  },
  {
    "path": "commons-hi/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<parent>\n\t\t<groupId>com.github.terran4j</groupId>\n\t\t<artifactId>terran4j-commons-parent</artifactId>\n\t\t<version>1.0.4-SNAPSHOT</version>\n\t</parent>\n\n\t<artifactId>terran4j-commons-hi</artifactId>\n\t<packaging>jar</packaging>\n\t<name>terran4j-commons-hi</name>\n\t<url>https://github.com/terran4j/commons</url>\n\n\t<dependencies>\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-api2doc</artifactId>\n        </dependency>\n\t\t\n\t\t<dependency>\n\t\t\t<groupId>org.apache.httpcomponents</groupId>\n\t\t\t<artifactId>httpcore</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.apache.httpcomponents</groupId>\n\t\t\t<artifactId>httpclient</artifactId>\n\t\t</dependency>\n    </dependencies>\n\t\n</project>\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/Action.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.google.gson.JsonElement;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class Action {\n\n    private HttpClient httpClient;\n\n    private String id;\n\n    private String name;\n\n    private String url;\n\n    private String postBody;\n\n    private String responseBody;\n\n    private JsonElement content;\n\n    private String method = RequestMethod.GET.name();\n\n    private Map<String, String> params = new HashMap<>();\n\n    private Map<String, String> headers = new HashMap<>();\n\n    private List<Write> writes = new ArrayList<>();\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public String getMethod() {\n        return method;\n    }\n\n    public void setMethod(String method) {\n        this.method = method;\n    }\n\n    public Map<String, String> getParams() {\n        return params;\n    }\n\n    public String param(String key) {\n        return String.valueOf(params.get(key));\n    }\n\n    public void setParams(Map<String, String> params) {\n        this.params = params;\n    }\n\n    public Map<String, String> getHeaders() {\n        return headers;\n    }\n\n    public void setHeaders(Map<String, String> headers) {\n        this.headers = headers;\n    }\n\n    public String header(String key) {\n        return String.valueOf(headers.get(key));\n    }\n\n    public HttpClient getHttpClient() {\n        return httpClient;\n    }\n\n    public void setHttpClient(HttpClient httpClient) {\n        this.httpClient = httpClient;\n    }\n\n    public List<Write> getWrites() {\n        return writes;\n    }\n\n    public void setWrites(List<Write> writes) {\n        this.writes = writes;\n    }\n\n    public String getPostBody() {\n        return postBody;\n    }\n\n    public void setPostBody(String postBody) {\n        this.postBody = postBody;\n    }\n\n    public String getResponseBody() {\n        return responseBody;\n    }\n\n    public void setResponseBody(String responseBody) {\n        this.responseBody = responseBody;\n    }\n\n    public JsonElement getContent() {\n        return content;\n    }\n\n    public void setContent(JsonElement content) {\n        this.content = content;\n    }\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/ApacheHttpClientBuilder.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.google.common.collect.Lists;\nimport org.apache.http.Header;\nimport org.apache.http.HttpHeaders;\nimport org.apache.http.HttpResponse;\nimport org.apache.http.StatusLine;\nimport org.apache.http.client.ClientProtocolException;\nimport org.apache.http.client.HttpClient;\nimport org.apache.http.client.HttpResponseException;\nimport org.apache.http.client.ResponseHandler;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.config.SocketConfig;\nimport org.apache.http.impl.client.HttpClients;\nimport org.apache.http.message.BasicHeader;\nimport org.apache.http.util.EntityUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.util.List;\n\npublic class ApacheHttpClientBuilder {\n\n    private static final Logger log = LoggerFactory.getLogger(ApacheHttpClientBuilder.class);\n\n    public static final String CHARSET = \"UTF-8\";\n\n    public static final HttpClient build(int timeout, String charset) {\n        SocketConfig socketConfig = SocketConfig.custom()\n                .setTcpNoDelay(true).build();\n        RequestConfig requestConfig = RequestConfig.custom()\n                .setConnectTimeout(timeout).setSocketTimeout(timeout)\n                .build();\n        List<Header> defaultHeaders = Lists.newArrayList();\n        defaultHeaders.add(new BasicHeader(HttpHeaders.ACCEPT_CHARSET, charset));\n\n        return HttpClients.custom().setDefaultHeaders(defaultHeaders)\n                .setDefaultRequestConfig(requestConfig)\n                .setDefaultSocketConfig(socketConfig)\n                .build();\n    }\n\n\n    public static class DefaultResponseHandler implements ResponseHandler<String> {\n\n        @Override\n        public String handleResponse(HttpResponse httpResponse) throws IOException {\n            StatusLine statusLine = httpResponse.getStatusLine();\n            if (statusLine.getStatusCode() >= 300) {\n                log.error(\"Http request failed, statusCode = {}, reasonPhrase = {}\",\n                        statusLine.getStatusCode(), statusLine.getReasonPhrase());\n                throw new HttpResponseException(statusLine.getStatusCode(),\n                        statusLine.getReasonPhrase());\n            }\n\n            if (httpResponse.getEntity() == null) {\n                throw new ClientProtocolException(\"Response contains no content\");\n            }\n\n            // get charset\n            Charset charset = Charset.forName(CHARSET);\n            // 有时第三方API返回的 contentType 也不一定对。\n//            ContentType responseContentType = ContentType.get(httpResponse.getEntity());\n//            if (responseContentType != null) {\n//                charset = responseContentType.getCharset();\n//            }\n\n            String content = EntityUtils.toString(httpResponse.getEntity(), charset);\n            return content;\n        }\n    }\n\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/Api2DocSupport.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.terran4j.commons.api2doc.meta.ClassMeta;\nimport com.terran4j.commons.api2doc.meta.MethodMeta;\nimport com.terran4j.commons.api2doc.meta.ParamMeta;\nimport com.terran4j.commons.util.Encoding;\nimport com.terran4j.commons.util.Jsons;\nimport org.apache.http.client.HttpClient;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.utils.URIBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.List;\n\npublic class Api2DocSupport {\n\n    private static final Logger log = LoggerFactory.getLogger(Api2DocSupport.class);\n\n    private static final Gson gson = new Gson();\n\n    public static final JsonObject loadConfig(String host, int port) throws IOException {\n        String url = \"http://\" + host + \":\" + port;\n        String api2docURL = url + \"/api2doc/meta/classes\";\n        JsonArray classMetas = loadClassMetas(api2docURL);\n        JsonObject config = toConfig(classMetas);\n        config.addProperty(\"url\", url);\n        return config;\n    }\n\n    static final JsonObject toConfig(JsonArray classMetas) {\n        JsonObject config = new JsonObject();\n\n        JsonArray actions = new JsonArray();\n        for (int i = 0; i < classMetas.size(); i++) {\n            JsonObject classMetaJson = classMetas.get(i).getAsJsonObject();\n            ClassMeta classMeta = gson.fromJson(classMetaJson, ClassMeta.class);\n\n            String classId = classMeta.getId();\n            List<MethodMeta> methods = classMeta.getMethods();\n            for (int j = 0; j < methods.size(); j++) {\n                MethodMeta method = methods.get(j);\n                JsonObject action = toAction(method, classId);\n                actions.add(action);\n            }\n        }\n\n        config.add(\"actions\", actions);\n        return config;\n    }\n\n    static final JsonObject toAction(MethodMeta method, String classId) {\n        JsonObject action = new JsonObject();\n\n        String methodId = method.getId();\n        String id = classId + \"-\" + methodId;\n        action.addProperty(\"id\", id);\n\n        String name = method.getName();\n        action.addProperty(\"name\", name);\n\n        String url = method.getPaths()[0];\n        action.addProperty(\"url\", url);\n\n        String requestMethod = method.getRequestMethods()[0];\n        action.addProperty(\"method\", requestMethod);\n\n        JsonObject headers = new JsonObject();\n        action.add(\"headers\", headers);\n\n        JsonObject params = new JsonObject();\n        action.add(\"params\", params);\n\n        List<ParamMeta> paramMetas = method.getParams();\n        for (ParamMeta paramMeta : paramMetas) {\n            String key = paramMeta.getId();\n            String value = \"{\" + key + \"}\";\n            String location = paramMeta.getLocation();\n            if (\"RequestHeader\".equals(location)) {\n                headers.addProperty(key, value);\n            }\n            if (\"PathVariable\".equals(location)) {\n                continue;\n            }\n            if (\"RequestParam\".equals(location)) {\n                params.addProperty(key, value);\n            }\n        }\n\n        return action;\n    }\n\n    static final JsonArray loadClassMetas(String url) throws IOException {\n        HttpClient httpClient = ApacheHttpClientBuilder.build(\n                1000 * 30, Encoding.UTF8.getName());\n\n\n        URIBuilder uriBuilder;\n        URI uri;\n        try {\n            uriBuilder = new URIBuilder(url);\n            uri = uriBuilder.build();\n        } catch (URISyntaxException e) {\n            throw new RuntimeException(\"build url failed: \" + url, e);\n        }\n        final HttpGet httpGet = new HttpGet(uri);\n        String response = httpClient.execute(httpGet,\n                new ApacheHttpClientBuilder.DefaultResponseHandler());\n        if (log.isInfoEnabled()) {\n            log.info(\"load api2doc meta info:{}\", response);\n        }\n\n        JsonObject root;\n        String resultCode;\n        JsonArray data;\n        try {\n            JsonElement json = Jsons.parseJson(response);\n            root = json.getAsJsonObject();\n            resultCode = root.get(\"resultCode\").getAsString();\n            data = root.get(\"data\").getAsJsonArray();\n        } catch (RuntimeException e) {\n            throw new RuntimeException(\"parse json failed: \" + e.getMessage(), e);\n        }\n\n        if (!\"success\".equals(resultCode)) {\n            String message = root.get(\"message\").getAsString();\n            throw new RuntimeException(\"load api2doc meta failed: \" + message);\n        }\n\n        return data;\n    }\n\n}"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/AssertResponseFailedException.java",
    "content": "package com.terran4j.commons.hi;\n\npublic class AssertResponseFailedException extends RuntimeException {\n\n\t/**\n\t * \n\t */\n\tprivate static final long serialVersionUID = -2832180150946360943L;\n\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/HttpClient.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.google.gson.*;\nimport com.terran4j.commons.util.Encoding;\nimport com.terran4j.commons.util.Strings;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.util.StringUtils;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class HttpClient {\n\n    private static final Logger log = LoggerFactory.getLogger(HttpClient.class);\n\n    private static final Gson gson = new Gson();\n\n    private ApplicationContext context;\n\n    private String host;\n\n    private int port;\n\n    private JsonObject config;\n\n    private List<HttpClientListener> listeners = new ArrayList<>();\n\n    /**\n     * 本地变量的初始值，其值是不能变的。\n     *  创建 session 时，会 copy 给 session ，session 中的 local 值可变。\n     */\n    private Map<String, String> locals = new HashMap<>();\n\n    private Map<String, Action> actions = new HashMap<>();\n\n    public void addListener(HttpClientListener listener) {\n        listeners.add(listener);\n    }\n\n    public List<HttpClientListener> getListeners() {\n        List<HttpClientListener> temp = new ArrayList<>();\n        temp.addAll(listeners);\n        return temp;\n    }\n\n    public static final HttpClient createByApi2Doc(\n            @NotNull String host, int port,\n            ApplicationContext context) throws IOException {\n        JsonObject config = Api2DocSupport.loadConfig(host, port);\n        return create(config, context, host, port);\n    }\n\n    public static final HttpClient create(\n            @NotNull Class<?> clazz, @NotNull String fileName,\n            ApplicationContext context) {\n        String json = Strings.getString(clazz, fileName);\n        if (StringUtils.isEmpty(json)) {\n            String msg = String.format(\n                    \"load resource[%s] in package[%s] failed.\",\n                    fileName, clazz.getPackage().getName());\n            throw new RuntimeException(msg);\n        }\n\n        JsonParser parser = new JsonParser();\n        JsonElement element = parser.parse(json);\n        JsonObject config = element.getAsJsonObject();\n\n        return create(config, context, \"localhost\", 8080);\n    }\n\n    public static final HttpClient create(\n            @NotNull JsonObject config,\n            ApplicationContext context, String host, int port) {\n        return new HttpClient(config, context, host, port);\n    }\n\n    public static final HttpClient create(@NotNull File file,\n                                          ApplicationContext context) {\n        InputStream in = null;\n        String json = null;\n        try {\n            in = new FileInputStream(file);\n            json = Strings.getString(in, Encoding.UTF8);\n        } catch (Exception e) {\n            log.error(\"load file[{}] error: {}\", file, e.getMessage(), e);\n        } finally {\n            if (in != null) {\n                try {\n                    in.close();\n                } catch (IOException e) {\n                }\n            }\n        }\n\n        if (StringUtils.isEmpty(json)) {\n            log.error(\"file[{}]  is empty\", file);\n            return null;\n        }\n\n        JsonParser parser = new JsonParser();\n        JsonElement element = parser.parse(json);\n        JsonObject config = element.getAsJsonObject();\n\n        return create(config, context, \"localhost\", 8080);\n    }\n\n    public static final HttpClient create(ApplicationContext context) {\n        return create(null, \"hi.json\", context);\n    }\n\n    private HttpClient(JsonObject config, ApplicationContext context, String host, int port) {\n        super();\n        this.host = host;\n        this.port = port;\n        setApplicationContext(context);\n        init(config);\n    }\n\n    public void setApplicationContext(ApplicationContext applicationContext) {\n        this.context = applicationContext;\n        if (log.isInfoEnabled()) {\n            log.info(\"setApplicationContext done.\");\n        }\n    }\n\n    private void init(JsonObject config) {\n\n        JsonElement element = config.get(\"locals\");\n        if (element != null) {\n            locals = gson.fromJson(element, Map.class);\n        }\n\n         element = config.get(\"actions\");\n        if (element != null) {\n            actions = new HashMap<>();\n            JsonArray array = element.getAsJsonArray();\n            for (int i = 0; i < array.size(); i++) {\n                Action invoker = gson.fromJson(array.get(i), Action.class);\n                invoker.setHttpClient(this);\n                actions.put(invoker.getId(), invoker);\n            }\n        }\n    }\n\n    public JsonObject getConfig() {\n        return config;\n    }\n\n    public void setConfig(JsonObject config) {\n        this.config = config;\n    }\n\n    public Map<String, String> cloneLocals() {\n        Map<String, String> cloneLocals = new HashMap<String, String>();\n        cloneLocals.putAll(locals);\n        return cloneLocals;\n    }\n\n    public Map<String, Action> getActions() {\n        return actions;\n    }\n\n    public Session createSession() {\n        return new Session(this, context);\n    }\n\n    public ApplicationContext getApplicationContext() {\n        return context;\n    }\n\n    public String getHost() {\n        return host;\n    }\n\n    public HttpClient setHost(String host) {\n        this.host = host;\n        return this;\n    }\n\n    public int getPort() {\n        return port;\n    }\n\n    public HttpClient setPort(int port) {\n        this.port = port;\n        return this;\n    }\n\n    public String local(String key) {\n        return String.valueOf(this.locals.get(key));\n    }\n\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/HttpClientListener.java",
    "content": "package com.terran4j.commons.hi;\n\npublic interface HttpClientListener {\n\n\tvoid beforeExecute(HttpRequest request);\n\t\n\tString afterExecute(HttpRequest request, String response);\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/HttpErrorCode.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.terran4j.commons.util.error.ErrorCode;\n\npublic enum HttpErrorCode implements ErrorCode {\n\t\n\tHTTP_REQUEST_ERROR(1, \"http.request.error\"),\n\t\n\tEXPECT_FAILED(2, \"expect.failed\"),\n\t\n\tACTION_NOT_FOUND(3, \"action.not.found\"),\n\t\n\tUNSUPPORTED_METHOD(4, \"unsupported.method\"),\n\t\n\tURI_SYNTAX_ERROR(5, \"uri.syntax.error\"),\n\t;\n\t\n\n\tprivate final int value;\n\t\n\tprivate final String name;\n\n\tprivate HttpErrorCode(int value, String name) {\n\t\tthis.value = value;\n\t\tthis.name = name;\n\t}\n\n\tpublic final int getValue() {\n\t\treturn value;\n\t}\n\t\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/HttpErrorCode.properties",
    "content": "\n\nhttp.request.error = Http request error, request as curl: \\n${curl}\nexpect.failed = Expect failed, expected: ${expectedValue}, but actual: ${actualValue}\naction.not.found = action[${action}] is not found. \nunsupported.method = Unsupported Method: ${method}, only supported methods: \\n${supportedMethods}\nuri.syntax.error = URI Syntax Error, please check: \\nurl = \\n${url}, \\nparams = ${params}"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/HttpException.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.terran4j.commons.util.error.BusinessException;\n\npublic class HttpException extends BusinessException {\n\n\tprivate static final long serialVersionUID = 3426759523831086859L;\n\n\tpublic HttpException(HttpErrorCode code) {\n\t\tsuper(code.getName());\n\t}\n\n\tpublic HttpException(HttpErrorCode code, Throwable cause) {\n\t\tsuper(code, cause);\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/HttpRequest.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.terran4j.commons.util.Jsons;\nimport org.apache.commons.collections4.MapUtils;\nimport org.apache.http.client.HttpClient;\nimport org.apache.http.client.ResponseHandler;\nimport org.apache.http.client.methods.*;\nimport org.apache.http.client.utils.URIBuilder;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URLEncoder;\nimport java.nio.charset.Charset;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\n\npublic final class HttpRequest {\n\n    private static final Logger log = LoggerFactory.getLogger(HttpRequest.class);\n\n    private final String url;\n\n    private final Map<String, String> headers = new HashMap<>();\n\n    private final Map<String, String> params = new HashMap<>();\n\n    private StringBuffer content = new StringBuffer();\n\n    private RequestMethod method = RequestMethod.GET;\n\n    private String postBody;\n\n    private String responseBody;\n\n    public RequestMethod getMethod() {\n        return method;\n    }\n\n    public HttpRequest setMethod(RequestMethod method) {\n        this.method = method;\n        return this;\n    }\n\n    private static HttpClient httpClient = ApacheHttpClientBuilder\n            .build(1000 * 60 * 10, ApacheHttpClientBuilder.CHARSET);\n\n\n    public HttpRequest(String url) {\n        super();\n        this.url = url;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public HttpRequest setParam(String key, String value) {\n        params.put(key, value);\n        return this;\n    }\n\n    public HttpRequest setParam(Map<String, String> params) {\n        this.params.putAll(params);\n        return this;\n    }\n\n    public HttpRequest setContent(String content) {\n        this.content = new StringBuffer(content);\n        return this;\n    }\n\n    public Map<String, String> getParams() {\n        return this.params;\n    }\n\n    public HttpRequest setHeader(String key, String value) {\n        headers.put(key, value);\n        return this;\n    }\n\n    public HttpRequest setHeaders(Map<String, String> headers) {\n        this.headers.putAll(headers);\n        return this;\n    }\n\n    public static final String toXml(Map<String, String> params) {\n        StringBuffer sb = new StringBuffer();\n        sb.append(\"<xml>\");\n        Iterator<String> it = params.keySet().iterator();\n        while (it.hasNext()) {\n            String key = it.next();\n            String value = params.get(key);\n            sb.append(\"\\t<\").append(key).append(\">\")\n                    .append(\"<![CDATA[\").append(value).append(\"]]>\")\n                    .append(\"</\").append(key).append(\">\");\n        }\n        sb.append(\"</xml>\");\n        return sb.toString();\n    }\n\n    private static final void addParams(URIBuilder uriBuilder, Map<String, String> params) {\n        if (params.size() > 0) {\n            Iterator<String> it = params.keySet().iterator();\n            while (it.hasNext()) {\n                String key = it.next();\n                String value = params.get(key);\n                uriBuilder.addParameter(key, value);\n            }\n        }\n    }\n\n    public String execute() throws HttpException {\n        HttpUriRequest httpRequest = null;\n        try {\n            if (method == RequestMethod.GET) {\n                URIBuilder uriBuilder = new URIBuilder(url);\n                addParams(uriBuilder, params);\n                URI uri = uriBuilder.build();\n                final HttpGet httpGet = new HttpGet(uri);\n                httpRequest = httpGet;\n            } else if (method == RequestMethod.POST) {\n                final HttpPost httpPost = new HttpPost(url);\n                if (this.content.length() > 0) {\n                    StringBuilder sb = new StringBuilder();\n                    sb.append(this.content);\n                    String content = sb.toString();\n                    if (log.isInfoEnabled()) {\n                        log.info(\"Http Post Content:\\n{}\", content);\n                    }\n                    ContentType contentType = ContentType.TEXT_PLAIN\n                            .withCharset(Charset.forName(ApacheHttpClientBuilder.CHARSET));\n                    httpPost.setEntity(new StringEntity(content, contentType));\n                    System.out.println(httpPost);\n                } else if (MapUtils.isNotEmpty(params)) {\n                    ContentType contentType = null;\n                    StringBuilder sb = new StringBuilder();\n                    if (\"XML\".equalsIgnoreCase(postBody)) {\n                        sb.append(toXml(params));\n                        contentType = ContentType.TEXT_XML\n                                .withCharset(Charset.forName(ApacheHttpClientBuilder.CHARSET));\n                    } else if(\"json\".equals(postBody)){\n                        sb.append(Jsons.toJsonText(params));\n                        contentType = ContentType.APPLICATION_FORM_URLENCODED\n                                .withCharset(Charset.forName(ApacheHttpClientBuilder.CHARSET));\n                    } else {\n                        sb.append(toUrlQuery(params));\n                        contentType = ContentType.APPLICATION_FORM_URLENCODED\n                                .withCharset(Charset.forName(ApacheHttpClientBuilder.CHARSET));\n                    }\n                    String content = sb.toString();\n                    if (log.isInfoEnabled()) {\n                        log.info(\"Http Post Body:\\n{}\", content);\n                    }\n                    httpPost.setEntity(new StringEntity(content, contentType));\n                }\n                httpRequest = httpPost;\n            } else if (method == RequestMethod.PUT) {\n                URIBuilder uriBuilder = new URIBuilder(url);\n                addParams(uriBuilder, params);\n                URI uri = uriBuilder.build();\n                final HttpPut httpPut = new HttpPut(uri);\n                httpRequest = httpPut;\n            } else if (method == RequestMethod.DELETE) {\n                URIBuilder uriBuilder = new URIBuilder(url);\n                addParams(uriBuilder, params);\n                URI uri = uriBuilder.build();\n                final HttpDelete httpDelete = new HttpDelete(uri);\n                httpRequest = httpDelete;\n            }\n        } catch (URISyntaxException e) {\n            throw new HttpException(HttpErrorCode.URI_SYNTAX_ERROR, e)\n                    .put(\"url\", url).put(\"params\", params)\n                    .as(HttpException.class);\n        } catch (JsonProcessingException e) {\n            throw new RuntimeException(e);\n        }\n        if (httpRequest == null) {\n            throw new HttpException(HttpErrorCode.UNSUPPORTED_METHOD)\n                    .put(\"method\", method)\n                    .put(\"supportedMethods\", new RequestMethod[]{\n                            RequestMethod.PUT, RequestMethod.GET, RequestMethod.POST\n                    })\n                    .setMessage(\"NOT supported method: ${method}\")\n                    .as(HttpException.class);\n        }\n\n        if (headers.size() > 0) {\n            Iterator<String> it = headers.keySet().iterator();\n            while (it.hasNext()) {\n                String key = it.next();\n                String value = headers.get(key);\n                httpRequest.addHeader(key, value);\n            }\n        }\n\n        ResponseHandler<String> responseHandler = new ApacheHttpClientBuilder.DefaultResponseHandler();\n\n        try {\n            long t0 = System.currentTimeMillis();\n            String response = httpClient.execute(httpRequest, responseHandler);\n            long t = System.currentTimeMillis() - t0;\n            if (log.isInfoEnabled()) {\n                log.info(\"request spend {}ms, response:\\n{}\", t, response);\n            }\n            return response;\n        } catch (Exception e) {\n            log.error(\"http failed: \" + e.getMessage(), e);\n            throw new HttpException(HttpErrorCode.HTTP_REQUEST_ERROR, e)\n                    .put(\"curl\", toCurl(httpRequest))\n                    .as(HttpException.class);\n        }\n    }\n\n    public static final String toUrlQuery(Map<String, String> params) {\n        StringBuffer sb = new StringBuffer();\n        boolean first = true;\n        Iterator<String> it = params.keySet().iterator();\n        while (it.hasNext()) {\n            String key = it.next();\n            String value = params.get(key);\n            if (!first) {\n                sb.append(\"&\");\n            }\n            sb.append(key).append(\"=\").append(encode(value));\n            first = false;\n        }\n        return sb.toString();\n    }\n\n\n    private static String encode(String value) {\n        try {\n            return URLEncoder.encode(value, ApacheHttpClientBuilder.CHARSET);\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private String toCurl(HttpUriRequest request) {\n        StringBuilder sb = new StringBuilder(\"curl\");\n        if (headers.size() > 0) {\n            Iterator<String> it = headers.keySet().iterator();\n            while (it.hasNext()) {\n                String key = it.next();\n                String value = headers.get(key);\n                sb.append(\" -H \\\"\").append(key).append(\": \").append(value).append(\"\\\"\");\n            }\n        }\n        if (request instanceof HttpPost) {\n            if (params.size() > 0) {\n                sb.append(\" -d \\\"\").append(toUrlQuery(params)).append(\"\\\"\");\n            }\n        }\n\n        sb.append(\" \\\"\").append(url);\n        if (request instanceof HttpGet) {\n            if (params.size() > 0) {\n                sb.append(\"?\").append(toUrlQuery(params));\n            }\n        }\n        sb.append(\"\\\"\");\n\n        return sb.toString();\n    }\n\n    public String getPostBody() {\n        return postBody;\n    }\n\n    public void setPostBody(String postBody) {\n        this.postBody = postBody;\n    }\n\n    public String getResponseBody() {\n        return responseBody;\n    }\n\n    public void setResponseBody(String responseBody) {\n        this.responseBody = responseBody;\n    }\n}"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/Param.java",
    "content": "package com.terran4j.commons.hi;\n\npublic class Param {\n\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/Request.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.config.ConfigElement;\nimport com.terran4j.commons.util.config.JsonConfigElement;\nimport com.terran4j.commons.util.config.XmlConfigElement;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport com.terran4j.commons.util.security.MD5Util;\nimport com.terran4j.commons.util.value.ValueSource;\nimport com.terran4j.commons.util.value.ValueSources;\nimport org.apache.commons.collections.MapUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.web.bind.annotation.RequestMethod;\n\nimport java.security.InvalidParameterException;\nimport java.util.*;\nimport java.util.concurrent.CountDownLatch;\n\npublic final class Request {\n\n    private final Session session;\n\n    private final ApplicationContext applicationContext;\n\n    private final Action action;\n\n    private final Map<String, String> params = new HashMap<>();\n\n    private final Map<String, String> inputs = new HashMap<>();\n\n    private String signParamKey = null;\n\n    private String signSecretKey = null;\n\n    private String signBuildKey = null;\n\n    /**\n     * add by mark for plain content.\n     */\n    private StringBuffer content = new StringBuffer();\n\n    private final ValueSources<String, String> context;\n\n    public Request(Action action, Session session, ApplicationContext applicationContext) {\n        super();\n        this.action = action;\n        this.session = session;\n        this.applicationContext = applicationContext;\n        this.context = buildContext();\n    }\n\n    private final ValueSources<String, String> buildContext() {\n        ValueSources<String, String> context = new ValueSources<>();\n\n        // 从 spring 环境配置中取值。\n        final ValueSource<String, String> springContext = key -> {\n            if (applicationContext == null) {\n                return null;\n            }\n            String value = applicationContext.getEnvironment().getProperty(key);\n            return value;\n        };\n        context.push(springContext);\n\n        // 从本地变量 locals 中取值。\n        final ValueSource<String, String> sessionContext = key -> {\n            String value = session.getLocals().get(key);\n            if (value == null) {\n                return null;\n            }\n            return Strings.format(value, springContext);\n        };\n        context.push(sessionContext);\n\n        // 从输入项中取值。\n        final ValueSource<String, String> inputContext = key -> {\n            String value = inputs.get(key);\n            return value;\n        };\n        context.push(inputContext);\n\n        return context;\n    }\n\n    public Request params(Properties props) {\n        if (props != null && !props.isEmpty()) {\n            Iterator<Object> it = props.keySet().iterator();\n            while (it.hasNext()) {\n                Object key = it.next();\n                Object value = props.get(key);\n                if (key instanceof String && value instanceof String) {\n                    params.put((String) key, (String) value);\n                }\n            }\n        }\n        return this;\n    }\n\n    public Request param(String key, String value) {\n        params.put(key, value);\n        return this;\n    }\n\n    public Request sign(String secretKey) {\n        return sign(secretKey, \"key\", \"sign\");\n    }\n\n    public Request sign(String secretKey, String buildKey, String paramKey) {\n        if (StringUtils.isBlank(secretKey)) {\n            throw new NullPointerException(\"secretKey is null.\");\n        }\n        if (StringUtils.isBlank(buildKey)) {\n            throw new NullPointerException(\"buildKey is null.\");\n        }\n        if (StringUtils.isBlank(paramKey)) {\n            throw new NullPointerException(\"paramKey is null.\");\n        }\n        this.signParamKey = paramKey.trim();\n        this.signSecretKey = secretKey.trim();\n        this.signBuildKey = buildKey.trim();\n        return this;\n    }\n\n    public Request content(String content) {\n        this.content = new StringBuffer(content);\n        return this;\n    }\n\n    public Request input(String key, String value) {\n        inputs.put(key, value);\n        return this;\n    }\n\n    public void exe(final int threadCount, final int exeCountPerThread,\n                    final int intervalTime) throws HttpException {\n        if (threadCount < 1) {\n            throw new InvalidParameterException(\n                    \"threadCount must more than 0: \" + threadCount);\n        }\n        if (exeCountPerThread < 1) {\n            throw new InvalidParameterException(\n                    \"exeCountPerThread must more than 0: \" + exeCountPerThread);\n        }\n        if (intervalTime < 0) {\n            throw new InvalidParameterException(\n                    \"intervalTime must more than -1: \" + intervalTime);\n        }\n        Thread[] threads = new Thread[threadCount];\n        final List<HttpException> errors = new ArrayList<HttpException>();\n        CountDownLatch latch = new CountDownLatch(threadCount);\n        for (int i = 0; i < threadCount; i++) {\n            threads[i] = new Thread(() -> {\n                try {\n                    for (int k = 0; k < exeCountPerThread; k++) {\n                        try {\n                            Thread.sleep(intervalTime);\n                        } catch (InterruptedException e) {\n                            // ignore.\n                        }\n                        try {\n                            exe();\n                        } catch (HttpException e) {\n                            errors.add(e);\n                        }\n                    }\n                } catch (Exception e) {\n                    e.printStackTrace();\n                } finally {\n                    latch.countDown();\n                }\n\n            });\n            threads[i].start();\n        }\n\n        // 等待所有线程结束。\n        try {\n            latch.await();\n        } catch (InterruptedException e) {\n        }\n\n        if (errors.size() > 0) {\n            for (HttpException error : errors) {\n                error.printStackTrace();\n            }\n        }\n    }\n\n    public String getContextValue(String key) {\n        return context.get(key);\n    }\n\n    public String parseValue(String value) {\n        if (StringUtils.isBlank(value)) {\n            return value;\n        }\n        return Strings.format(value, context, \"{\", \"}\", null);\n    }\n\n    public String getActualURL() {\n        String url = action.getUrl();\n        String actualURL = parseValue(url);\n        if (!actualURL.startsWith(\"http\")) {\n            actualURL = getURLPrefix() + actualURL;\n        }\n        return actualURL;\n    }\n\n    private String getURLPrefix() {\n        HttpClient httpClient = action.getHttpClient();\n        int port = httpClient.getPort();\n        if (port == 80) {\n            return \"http://\" + httpClient.getHost();\n        } else {\n            return \"http://\" + httpClient.getHost() + \":\" + port;\n        }\n    }\n\n    /**\n     * @return 实际的参数值\n     */\n    public Map<String, String> getActualParams() throws BusinessException {\n        // 从入参中取值。\n        final Map<String, String> actualParams = new HashMap<>();\n        Iterator<String> it = params.keySet().iterator();\n        while (it.hasNext()) {\n            String key = it.next();\n            String value = params.get(key);\n            if (value != null) {\n                String actualValue = parseValue(value);\n                actualParams.put(key, actualValue);\n            }\n        }\n\n        Map<String, String> actionParams = parseValues(action.getParams());\n        actionParams.putAll(actualParams);\n\n        buildSignParam(actionParams);\n\n        return actionParams;\n    }\n\n    /**\n     * @return\n     */\n    public StringBuffer getActualContent() {\n        StringBuffer sb = new StringBuffer();\n        sb.append(this.content);\n        sb.trimToSize();\n        return sb;\n    }\n\n    public Map<String, String> getActualHeaders() {\n        Map<String, String> headers = action.getHeaders();\n        return parseValues(headers);\n    }\n\n    private Map<String, String> parseValues(Map<String, String> map) {\n        Map<String, String> newMap = new HashMap<>();\n        if (map != null) {\n            Iterator<String> it = map.keySet().iterator();\n            while (it.hasNext()) {\n                String key = it.next();\n                String value = map.get(key);\n                String actualValue = parseValue(value);\n                newMap.put(key, actualValue);\n            }\n        }\n        return newMap;\n    }\n\n    private void buildSignParam(Map<String, String> params)\n            throws BusinessException {\n        if (signSecretKey == null || signParamKey == null || signBuildKey == null) {\n            return;\n        }\n\n        if (MapUtils.isEmpty(params)) {\n            return;\n        }\n\n        if (params.containsKey(signParamKey)) {\n            throw new BusinessException(ErrorCodes.INVALID_PARAM)\n                    .put(\"signParamKey\", signParamKey)\n                    .setMessage(\"参数key与签名参数重复：${signParamKey}\");\n        }\n\n        String secretKey = String.format(\"&%s=%s\", signBuildKey, signSecretKey);\n        String signValue = MD5Util.signature(params, secretKey).toUpperCase();\n        params.put(signParamKey, signValue);\n\n    }\n\n    public Response exe() throws BusinessException {\n\n        // 获取实际的 URL。\n        String actualURL = getActualURL();\n        HttpRequest request = new HttpRequest(actualURL);\n\n\n        // 获取实际的入参。\n        final Map<String, String> actualParams = getActualParams();\n        request.setParam(actualParams);\n        request.setContent(this.content.toString());\n\n        RequestMethod method = RequestMethod.GET;\n        String methodName = action.getMethod();\n        if (StringUtils.isNotBlank(methodName)) {\n            method = RequestMethod.valueOf(methodName);\n            if (method == null) {\n                String msg = String.format(\"http method[%s] not supported in action: %s\",\n                        methodName, action.getId());\n                throw new UnsupportedOperationException(msg);\n            }\n        }\n        request.setMethod(method);\n\n        request.setPostBody(action.getPostBody());\n        request.setResponseBody(action.getResponseBody());\n        if(action.getContent()!=null)request.setContent(parseValue(action.getContent().toString()));\n\n        Map<String, String> actualHeaders = getActualHeaders();\n        request.setHeaders(actualHeaders);\n\n        List<HttpClientListener> listeners = action.getHttpClient().getListeners();\n        for (HttpClientListener listener : listeners) {\n            listener.beforeExecute(request);\n        }\n        String response = request.execute();\n        for (HttpClientListener listener : listeners) {\n            response = listener.afterExecute(request, response);\n        }\n\n        ConfigElement root = null;\n        if (\"XML\".equalsIgnoreCase(request.getResponseBody())) {\n            root = new XmlConfigElement(response);\n        } else {\n            root = new JsonConfigElement(response);\n        }\n\n        List<Write> writes = action.getWrites();\n        if (writes != null && writes.size() > 0) {\n            context.push(root);\n            for (Write write : writes) {\n                write.doWrite(session, context);\n            }\n            context.pop();\n        }\n\n        return new Response(root, session);\n    }\n\n}"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/Response.java",
    "content": "package com.terran4j.commons.hi;\n\nimport com.terran4j.commons.util.config.ConfigElement;\nimport org.springframework.util.Assert;\nimport org.springframework.util.StringUtils;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.terran4j.commons.util.value.JsonValueSource;\n\npublic final class Response {\n\t\n\tprivate final Gson gson = new Gson();\n\t\n\tprivate final Session session;\n\n\tprivate final ConfigElement result;\n\t\n\tpublic Response(ConfigElement result, Session session) {\n\t\tsuper();\n\t\tthis.session = session;\n\t\tthis.result = result;\n\t}\n\t\n\tpublic ConfigElement getResult() {\n\t\treturn result;\n\t}\n\n//    public <T> T getResult(Class<T> clazz) {\n//\t    if (result == null) {\n//\t        return null;\n//        }\n//        return result.createObject();\n//    }\n//\n//\tpublic JsonElement getJson(String key) {\n//\t\tJsonElement jsonElement = values.getElement(key);\n//\t\treturn jsonElement;\n//\t}\n//\n//\tpublic JsonElement getJson(String key, int index) {\n//\t\tJsonElement jsonElement = values.getElement(key);\n//\t\tif (!jsonElement.isJsonArray()) {\n//\t\t\treturn null;\n//\t\t}\n//\t\treturn jsonElement.getAsJsonArray().get(index);\n//\t}\n//\n//\tpublic <T> T getObject(String key, Class<T> clazz) {\n//\t\tJsonElement jsonElement = values.getElement(key);\n//\t\tif (jsonElement == null) {\n//\t\t\treturn null;\n//\t\t}\n//\t\treturn gson.fromJson(jsonElement, clazz);\n//\t}\n\n//\n//\tpublic Response assertEqual(String key, String expectValue) {\n//\t\tString actualValue = values.get(key);\n//\t\torg.junit.Assert.assertEquals(expectValue, actualValue);\n//\t\treturn this;\n//\t}\n//\n//\tpublic Response assertContains(String key) {\n//\t\tString actualValue = values.get(key);\n//\t\tAssert.isTrue(!StringUtils.isEmpty(actualValue), \"reponse should contains key: \" + key);\n//\t\treturn this;\n//\t}\n//\n//\tpublic Response save(String key, String alias) {\n//\t\tString actualValue = values.get(key);\n//\t\tsession.setAttribute(alias, actualValue);\n//\t\treturn this;\n//\t}\n//\n//\tpublic Response save(String key) {\n//\t\tString actualValue = values.get(key);\n//\t\tsession.setAttribute(key, actualValue);\n//\t\treturn this;\n//\t}\n//\n//\tpublic String getByPath(String path) {\n//\t\treturn result.get(path);\n//\t}\n//\n//\tpublic long getByPath(String path, long defaultValue) {\n//\t\ttry {\n//\t\t\treturn Long.parseLong(values.get(path));\n//\t\t} catch (NumberFormatException e) {\n//\t\t\treturn defaultValue;\n//\t\t}\n//\t}\n\n}"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/Session.java",
    "content": "package com.terran4j.commons.hi;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.springframework.context.ApplicationContext;\n\npublic final class Session {\n\n\tprivate final HttpClient httpClient;\n\n\tprivate final ApplicationContext applicationContext;\n\t\n//\tprivate Map<String, String> params = new HashMap<>();\n//\n//\tprivate Map<String, String> headers = new HashMap<>();\n//\n//\tprivate final Map<String, String> attrs = new HashMap<>();\n\t\n\tprivate Map<String, String> locals = new HashMap<>();\n\n\tSession(HttpClient service, ApplicationContext context) {\n\t\tsuper();\n\t\tthis.httpClient = service;\n\t\tthis.applicationContext = context;\n        this.locals = service.cloneLocals();\n//\t\tthis.params = service.cloneParams();\n//\t\tthis.headers = service.cloneHeaders();\n\t}\n//\n//\tpublic Map<String, String> getActualParams() {\n//\t\treturn params;\n//\t}\n\n    public HttpClient getHttpClient() {\n        return httpClient;\n    }\n\t\n\tpublic Map<String, String> getLocals() {\n\t\treturn locals;\n\t}\n\n//\tpublic Map<String, String> getHeaders() {\n//\t\treturn headers;\n//\t}\n\n\tpublic Request createRequest(String action) throws HttpException {\n\t\tAction actionObject = httpClient.getActions().get(action);\n\t\tif (actionObject == null) {\n\t\t\tthrow new HttpException(HttpErrorCode.ACTION_NOT_FOUND)\n\t\t\t\t\t.put(\"action\", action).as(HttpException.class);\n\t\t}\n\t\tRequest request = new Request(actionObject, this, applicationContext);\n\t\treturn request;\n\t}\n\t\n\tpublic HttpClient getService() {\n\t\treturn httpClient;\n\t}\n\t\n\tpublic Session local(String key, String value) {\n\t\tthis.locals.put(key, value);\n\t\treturn this;\n\t}\n\n\tpublic String local(String key) {\n\t    return this.locals.get(key);\n    }\n}"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/WrappedResponse.java",
    "content": "package com.terran4j.commons.hi;\n\nimport java.util.UUID;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.terran4j.commons.util.error.BusinessException;\n\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic class WrappedResponse {\n\t\n    public static final int SUCCESS_CODE = 0;\n    \n    public static final String SUCCESS_MESSAGE = \"success\";\n    \n    public static final String KEY_resultCode = \"resultCode\";\n    \n    public static final String KEY_message = \"message\";\n    \n    public static final String KEY_data = \"data\";\n\n    private String requestId = UUID.randomUUID().toString();\n\n    private long serverTime = System.currentTimeMillis();\n\n    private int resultCode;\n\n    private Object data;\n    \n    private String message;\n    \n    public static WrappedResponse success() {\n    \t\tWrappedResponse response = new WrappedResponse();\n        response.setResultCode(SUCCESS_CODE);\n        response.setMessage(SUCCESS_MESSAGE);\n        return response;\n    }\n    \n    public static WrappedResponse success(Object data) {\n        WrappedResponse response = new WrappedResponse();\n        response.setResultCode(SUCCESS_CODE);\n        response.setMessage(SUCCESS_MESSAGE);\n        response.setData(data);\n        return response;\n    }\n    \n    public static WrappedResponse fail(BusinessException e) {\n        WrappedResponse response = new WrappedResponse();\n        response.setResultCode(e.getErrorCode().getValue());\n        response.setMessage(e.getMessage());\n        response.setData(e.getProps());\n        return response;\n    }\n\n    public String getRequestId() {\n        return requestId;\n    }\n\n    public void setRequestId(String requestId) {\n        this.requestId = requestId;\n    }\n\n    public long getServerTime() {\n        return serverTime;\n    }\n\n    public void setServerTime(long serverTime) {\n        this.serverTime = serverTime;\n    }\n\n    public int getResultCode() {\n        return resultCode;\n    }\n\n    public void setResultCode(int resultCode) {\n        this.resultCode = resultCode;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n    public Object getData() {\n        return data;\n    }\n\n    public void setData(Object data) {\n        this.data = data;\n    }\n\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/Write.java",
    "content": "package com.terran4j.commons.hi;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.value.ValueSources;\n\npublic class Write {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(Write.class);\n\n\tprivate String key;\n\t\n\tprivate String value;\n\t\n//\tprivate String to;\n\n\tpublic String getKey() {\n\t\treturn key;\n\t}\n\n\tpublic void setKey(String key) {\n\t\tthis.key = key;\n\t}\n\n\tpublic String getValue() {\n\t\treturn value;\n\t}\n\n\tpublic void setValue(String value) {\n\t\tthis.value = value;\n\t}\n\n//\tpublic String getTo() {\n//\t\treturn to;\n//\t}\n//\n//\tpublic void setTo(String to) {\n//\t\tthis.to = to;\n//\t}\n\n\tpublic void doWrite(Session session, final ValueSources<String, String> values) {\n\t\tString actualValue = Strings.format(value, values);\n//\t\tWriteTo writeTo = WriteTo.valueOf(to);\n//\t\tif (writeTo == WriteTo.headers) {\n//\t\t\tsession.getHeaders().put(key, actualValue);\n//\t\t\tif (log.isInfoEnabled()) {\n//\t\t\t\tlog.info(\"write to {}[{} = {}]\", to, key, actualValue);\n//\t\t\t}\n//\t\t}\n        session.getLocals().put(key, actualValue);\n        if (log.isInfoEnabled()) {\n            log.info(\"write to {}[{} = {}]\", key, actualValue);\n        }\n//\t\tif (writeTo == WriteTo.locals) {\n//\n//\t\t}\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-hi/src/main/java/com/terran4j/commons/hi/WriteTo.java",
    "content": "package com.terran4j.commons.hi;\n\npublic enum WriteTo {\n\n\tparams, headers, cookies, locals;\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/application.yml",
    "content": "\n\nHttpClientTest:\n  k1: 111\n\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/demo/hi/Calculator.java",
    "content": "package com.terran4j.demo.hi;\n\npublic class Calculator {\n\n\tprivate final long a;\n\t\n\tprivate final long b;\n\t\n\tprivate long result;\n\n\tpublic Calculator(long a, long b) {\n\t\tsuper();\n\t\tthis.a = a;\n\t\tthis.b = b;\n\t}\n\t\n\tpublic long getA() {\n\t\treturn a;\n\t}\n\n\tpublic long getB() {\n\t\treturn b;\n\t}\n\n\tpublic long getResult() {\n\t\treturn result;\n\t}\n\n\tpublic Calculator plus() {\n\t\tresult = a + b;\n\t\treturn this;\n\t}\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/demo/hi/CalculatorController.java",
    "content": "package com.terran4j.demo.hi;\n\nimport org.springframework.web.bind.annotation.*;\n\n@RequestMapping(\"/calculator\")\n@RestController\npublic class CalculatorController {\n\n\t@RequestMapping(\"/plus\")\n\t@ResponseBody\n\tpublic Calculator plus(@RequestParam(\"a\") long a, @RequestParam(\"b\") long b) {\n\t\treturn new Calculator(a, b).plus();\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/demo/hi/HttpClientApp.java",
    "content": "package com.terran4j.demo.hi;\n\nimport com.terran4j.commons.hi.HttpClient;\nimport com.terran4j.commons.hi.HttpException;\nimport com.terran4j.commons.hi.Response;\nimport com.terran4j.commons.hi.Session;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.junit.Assert;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.ApplicationContext;\n\n@SpringBootApplication\npublic class HttpClientApp {\n\n    public static void main(String[] args) throws BusinessException {\n        ApplicationContext context = SpringApplication.run(HttpClientApp.class, args);\n\n        // 创建一个 httpClient 对象，它会加载 http.config.json 文件中定义的 HTTP API 信息。\n        HttpClient httpClient = HttpClient.create(\n                HttpClientApp.class, \"demo.json\", context);\n\n        // 创建一个 session，它会在客户端维护一些会话信息。\n        Session session = httpClient.createSession();\n\n        // 调用 plus 接口，输入参数 input = 3.\n        Response response = session.createRequest(\"plus\").param(\"input\", \"3\").exe();\n\n        // 从返回结果中，取 result 字段的值。\n        int total = response.getResult().attr(\"result\", 0);\n        Assert.assertEquals(3, total);\n\n        // 再调用一次，发现返回结果的确在累加。\n        response = session.createRequest(\"plus\").param(\"input\", \"5\").exe();\n        total = response.getResult().attr(\"result\", 0);\n        Assert.assertEquals(8, total);\n    }\n\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/demo/hi/demo.json",
    "content": "{\n  \"locals\": {\n    \"total\": 0\n  },\n  \"actions\": [\n    {\n      \"id\": \"plus\",\n      \"name\": \"两数相加\",\n      \"url\": \"/calculator/plus\",\n      \"method\": \"POST\",\n      \"params\": {\n        \"a\": \"{total}\",\n        \"b\": \"{input}\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/BaseHiTest.java",
    "content": "package com.terran4j.test.hi;\n\nimport com.terran4j.commons.hi.HttpClient;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\n@RunWith(SpringJUnit4ClassRunner.class)\n@SpringBootTest(\n        classes = {TestHiApp.class},\n        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT\n)\npublic class BaseHiTest {\n\n    @Autowired\n    protected ApplicationContext context;\n\n    protected final HttpClient create() {\n        return HttpClient.create(HttpClientTest.class,\n                this.getClass().getSimpleName() + \".json\",\n                context);\n    }\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/HttpClientTest.java",
    "content": "package com.terran4j.test.hi;\n\nimport com.terran4j.commons.hi.Action;\nimport com.terran4j.commons.hi.HttpClient;\nimport com.terran4j.commons.hi.Request;\nimport com.terran4j.commons.hi.Session;\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class HttpClientTest extends BaseHiTest {\n\n    /**\n     * 加载 hi 的配置，测试加载到的数据是否正确。\n     *\n     * @throws Exception\n     */\n    @Test\n    public void testLoadConfig() throws Exception {\n        HttpClient client = create();\n        Assert.assertEquals(\"0\", client.local(\"total\"));\n\n        Action plusAction = client.getActions().get(\"plus\");\n        Assert.assertEquals(\"plus\", plusAction.getId());\n        Assert.assertEquals(\"两数相加\", plusAction.getName());\n        Assert.assertEquals(\"/calculator/plus\", plusAction.getUrl());\n        Assert.assertEquals(\"POST\", plusAction.getMethod());\n\n        Assert.assertEquals(\"{total}\", plusAction.param(\"a\"));\n        Assert.assertEquals(\"{number}\", plusAction.param(\"b\"));\n\n        Assert.assertEquals(\"{token}\", plusAction.header(\"token\"));\n    }\n\n    /**\n     * session 可以维持本地数据，\n     * 但不同的 session 不受影响。\n     *\n     * @throws Exception\n     */\n    @Test\n    public void testSessionLocal() throws Exception {\n        HttpClient client = create();\n        Session session = client.createSession();\n        Assert.assertEquals(\"0\", session.local(\"total\"));\n\n        session.local(\"total\", \"10\");\n        Assert.assertEquals(\"10\", session.local(\"total\"));\n\n        Session session2 = client.createSession();\n        Assert.assertEquals(\"0\", session2.local(\"total\"));\n    }\n\n    /**\n     * 从 request 中获取上下文数据。\n     *\n     * @throws Exception\n     */\n    @Test\n    public void testGetContextValue() throws Exception {\n        HttpClient client = create();\n        Session session = client.createSession();\n        Request request = session.createRequest(\"plus\");\n\n        Assert.assertEquals(\"abc\", request.getContextValue(\"token\"));\n\n        request.input(\"token\", \"123\");\n        Assert.assertEquals(\"123\", request.getContextValue(\"token\"));\n    }\n\n    /**\n     * 从 request 中获取实际的 URL 及参数、Header 等信息。\n     *\n     * @throws Exception\n     */\n    @Test\n    public void testRequestInput() throws Exception {\n        HttpClient client = create();\n        Session session = client.createSession();\n        Request request = session.createRequest(\"plus\");\n\n        String url = \"http://localhost:8080/calculator/plus\";\n        Assert.assertEquals(url, request.getActualURL());\n\n        Assert.assertEquals(\"abc\", request.getActualHeaders().get(\"token\"));\n        request.input(\"token\", \"123\");\n        Assert.assertEquals(\"123\", request.getActualHeaders().get(\"token\"));\n\n        Assert.assertEquals(\"{number}\", request.getActualParams().get(\"b\"));\n        request.input(\"number\", \"5\");\n        Assert.assertEquals(\"5\", request.getActualParams().get(\"b\"));\n    }\n\n    /**\n     * 从 request 中获取实际的 URL 及参数、Header 等信息。\n     *\n     * @throws Exception\n     */\n    @Test\n    public void testRequestContent() throws Exception {\n        HttpClient client = create();\n        Session session = client.createSession();\n        Request request = session.createRequest(\"plus\");\n\n        String url = \"http://localhost:8080/calculator/plus\";\n        Assert.assertEquals(url, request.getActualURL());\n\n        request.content(\"{a=1,b=5}\");\n\n        Assert.assertEquals(\"{a=1,b=5}\",\n                request.getActualContent().toString());\n\n    }\n\n    /**\n     * 从 request 中获取实际的 URL 及参数、Header 等信息。\n     *\n     * @throws Exception\n     */\n    @Test\n    public void testSign() throws Exception {\n        HttpClient client = create();\n        Session session = client.createSession();\n        Request request = session.createRequest(\"plus\");\n        String secretKey = \"4BLYkNxktpqJSTdBf9n1IS9AQORFlqpa\";\n        request.input(\"number\", \"5\").sign(secretKey);\n        String sign = request.getActualParams().get(\"sign\");\n        Assert.assertEquals(\"30A2CBD764D159B18141DB8B0FB2094F\", sign);\n    }\n}"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/HttpClientTest.json",
    "content": "{\n  \"locals\": {\n    \"total\": \"0\",\n    \"token\": \"abc\"\n  },\n  \"actions\": [\n    {\n      \"id\": \"plus\",\n      \"name\": \"两数相加\",\n      \"url\": \"/calculator/plus\",\n      \"method\": \"POST\",\n      \"headers\": {\n        \"token\": \"{token}\"\n      },\n      \"params\": {\n        \"a\": \"{total}\",\n        \"b\": \"{number}\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/TestHiApp.java",
    "content": "package com.terran4j.test.hi;\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class TestHiApp {\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/api2doc/Api2DocApp.java",
    "content": "package com.terran4j.test.hi.api2doc;\n\nimport com.terran4j.commons.api2doc.config.EnableApi2Doc;\nimport com.terran4j.commons.restpack.EnableRestPack;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@EnableRestPack\n@EnableApi2Doc\n@SpringBootApplication\npublic class Api2DocApp {\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/api2doc/Api2DocDemoController.java",
    "content": "package com.terran4j.test.hi.api2doc;\n\nimport com.terran4j.commons.api2doc.annotations.Api2Doc;\nimport com.terran4j.commons.restpack.RestPackController;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\n@Api2Doc(\"test\")\n@RestPackController\n@RequestMapping(\"/test\")\npublic class Api2DocDemoController {\n\n    @RequestMapping(value = \"/multiply\", method = RequestMethod.POST)\n    public MultiplyObject multiply(long a, long b) {\n        return new MultiplyObject(a, b);\n    }\n\n    @RequestMapping(value = \"/echo\", method = RequestMethod.GET)\n    public String echo(String msg) {\n        return msg;\n    }\n\n    @RequestMapping(value = \"/put/{id}\", method = RequestMethod.PUT)\n    @ResponseBody\n    public String put(Long id, String name) {\n        return id + \"-\" + name;\n    }\n\n    @RequestMapping(value = \"/delete\", method = RequestMethod.DELETE)\n    @ResponseBody\n    public void delete(Long id) {\n    }\n\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/api2doc/Api2DocSupportTest.java",
    "content": "package com.terran4j.test.hi.api2doc;\n\nimport com.terran4j.commons.hi.HttpClient;\nimport com.terran4j.commons.hi.Request;\nimport com.terran4j.commons.hi.Response;\nimport com.terran4j.commons.hi.Session;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport java.io.IOException;\n\n@RunWith(SpringJUnit4ClassRunner.class)\n@SpringBootTest(classes = {Api2DocApp.class},\n        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)\npublic class Api2DocSupportTest {\n\n    @Autowired\n    protected ApplicationContext context;\n\n    private HttpClient createClient() {\n        try {\n            return HttpClient.createByApi2Doc(\n                    \"localhost\", 8080, context);\n        } catch (IOException e) {\n            e.printStackTrace();\n            Assert.fail(e.getMessage());\n            return null;\n        }\n    }\n\n    @Test\n    public void testEcho() throws Exception {\n        Session session = createClient().createSession();\n        Request request = session.createRequest(\"test-echo\");\n        Response response = request.input(\"msg\", \"hello\").exe();\n        String result = response.getResult().get(\"data\");\n        Assert.assertEquals(\"hello\", result);\n    }\n\n    @Test\n    public void testMultiply() throws Exception {\n        Session session = createClient().createSession();\n        Request request = session.createRequest(\"test-multiply\");\n        Response response = request.input(\"a\", \"2\").input(\"b\", \"3\").exe();\n\n        // 用对象来接收。\n        MultiplyObject plusObject = response.getResult().getChild(\"data\")\n                .asObject(MultiplyObject.class);\n        Assert.assertEquals(2, plusObject.getA());\n        Assert.assertEquals(3, plusObject.getB());\n        Assert.assertEquals(6, plusObject.getResult());\n    }\n\n    @Test\n    public void testPut() throws Exception {\n        Session session = createClient().createSession();\n        Request request = session.createRequest(\"test-put\");\n        Response response = request.input(\"id\", \"1\").input(\"name\", \"abc\").exe();\n        String data = response.getResult().attr(\"data\");\n        Assert.assertEquals(\"1-abc\", data);\n    }\n\n    @Test\n    public void testDelete() throws Exception {\n        Session session = createClient().createSession();\n        Request request = session.createRequest(\"test-delete\");\n        Response response = request.input(\"id\", \"1\").exe();\n        String resultCode = response.getResult().attr(\"resultCode\");\n        Assert.assertEquals(\"success\", resultCode);\n    }\n\n}"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/api2doc/MultiplyObject.java",
    "content": "package com.terran4j.test.hi.api2doc;\n\npublic class MultiplyObject {\n\n    private long a;\n\n    private long b;\n\n    private long result;\n\n    public MultiplyObject(long a, long b) {\n        this.a = a;\n        this.b = b;\n        result = a * b;\n    }\n\n    public long getA() {\n        return a;\n    }\n\n    public void setA(long a) {\n        this.a = a;\n    }\n\n    public long getB() {\n        return b;\n    }\n\n    public void setB(long b) {\n        this.b = b;\n    }\n\n    public long getResult() {\n        return result;\n    }\n\n    public void setResult(long result) {\n        this.result = result;\n    }\n\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/exe/ExeController.java",
    "content": "package com.terran4j.test.hi.exe;\n\nimport org.springframework.web.bind.annotation.*;\n\nimport javax.servlet.http.HttpServletRequest;\n\n@RequestMapping(\"/exe\")\n@RestController\npublic class ExeController {\n\n    @RequestMapping(value = \"/plus\", method = RequestMethod.POST)\n    @ResponseBody\n    public PlusObject plus(long a, long b) {\n        return new PlusObject(a, b);\n    }\n\n    @RequestMapping(value = \"/echo\", method = RequestMethod.GET)\n    @ResponseBody\n    public String echo(HttpServletRequest request) {\n        return request.getQueryString();\n    }\n\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/exe/ExeTest.java",
    "content": "package com.terran4j.test.hi.exe;\n\nimport com.terran4j.commons.hi.Request;\nimport com.terran4j.commons.hi.Response;\nimport com.terran4j.commons.hi.Session;\nimport com.terran4j.commons.util.config.ConfigElement;\nimport com.terran4j.test.hi.BaseHiTest;\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class ExeTest extends BaseHiTest {\n\n    @Test\n    public void testEcho() throws Exception {\n        Session session = create().createSession();\n        Request request = session.createRequest(\"echo\");\n        Response response = request.param(\"k1\", \"1\").param(\"k2\", \"2\").exe();\n\n        String result = response.getResult().getValue();\n        Assert.assertEquals(\"k1=1&k2=2\", result);\n    }\n\n    @Test\n    public void testPlus() throws Exception {\n        Session session = create().createSession();\n        Assert.assertEquals(\"0\", session.local(\"result\"));\n\n        Request request = session.createRequest(\"plus\");\n        Response response = request.input(\"a\", \"2\").input(\"b\", \"3\").exe();\n        ConfigElement result = response.getResult();\n\n        // 用对象来接收。\n        PlusObject plusObject = result.asObject(PlusObject.class);\n        Assert.assertEquals(2, plusObject.getA());\n        Assert.assertEquals(3, plusObject.getB());\n        Assert.assertEquals(5, plusObject.getSum());\n\n        // 用 getByPath 获取结果中的数据。\n        Assert.assertEquals(5, result.attr(\"sum\", 0));\n\n        // plus 方法将 结果写到 session 中去了。\n        Assert.assertEquals(\"5\", session.local(\"result\"));\n    }\n\n}"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/exe/ExeTest.json",
    "content": "{\n  \"locals\": {\n    \"result\": \"0\"\n  },\n  \"actions\": [\n    {\n      \"id\": \"plus\",\n      \"name\": \"两数相加\",\n      \"url\": \"/exe/plus\",\n      \"method\": \"POST\",\n      \"params\": {\n        \"a\": \"{a}\",\n        \"b\": \"{b}\"\n      },\n      \"writes\": [\n        { \"key\": \"result\", \"value\": \"${sum}\" }\n      ]\n    },\n    {\n      \"id\": \"echo\",\n      \"name\": \"重复返回\",\n      \"url\": \"/exe/echo\",\n      \"method\": \"GET\"\n    }\n  ]\n}"
  },
  {
    "path": "commons-hi/src/test/java/com/terran4j/test/hi/exe/PlusObject.java",
    "content": "package com.terran4j.test.hi.exe;\n\npublic class PlusObject {\n\n    private long a;\n\n    private long b;\n\n    private long sum;\n\n    public PlusObject(long a, long b) {\n        this.a = a;\n        this.b = b;\n        sum = a + b;\n    }\n\n    public long getA() {\n        return a;\n    }\n\n    public void setA(long a) {\n        this.a = a;\n    }\n\n    public long getB() {\n        return b;\n    }\n\n    public void setB(long b) {\n        this.b = b;\n    }\n\n    public long getSum() {\n        return sum;\n    }\n\n    public void setSum(long sum) {\n        this.sum = sum;\n    }\n\n}\n"
  },
  {
    "path": "commons-hi/src/test/java/example.json",
    "content": "{\n  \"actions\": [\n    {\n      \"id\": \"plus\",\n      \"name\": \"两数相加\",\n      \"url\": \"{url}/calculator/plus\",\n      \"method\": \"POST\",\n      \"headers\": {\n        \"k1\": \"v1\",\n        \"k2\": \"v2\"\n      },\n      \"params\": {\n        \"a\": \"{total}\",\n        \"b\": \"{input}\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "commons-hi/src/test/java/hi.json",
    "content": "{\n  \"actions\": [\n    {\n      \"id\": \"plus\",\n      \"name\": \"两数相加\",\n      \"url\": \"/calculator/plus\",\n      \"method\": \"POST\",\n      \"params\": {\n        \"a\": \"{total}\",\n        \"b\": \"{input}\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "commons-hi/src/test/java/http.config.json",
    "content": "{\n\t\"locals\": {\n\t\t\"total\": \"0\"\n\t},\n\t\"actions\": [\n\t\t{\n\t\t\t\"id\": \"plus\",\n\t\t\t\"name\": \"两数相加\",\n\t\t\t\"url\": \"${url}/calculator/plus\",\n\t\t\t\"method\": \"POST\",\n\t\t\t\"params\": {\n\t\t\t\t\"a\": \"${total}\",\n\t\t\t\t\"b\": \"${input}\"\n\t\t\t},\n\t\t\t\"writes\": [\n\t\t\t\t{ \"to\": \"locals\", \"key\": \"total\", \"value\": \"${result}\" }\n\t\t\t]\n\t\t}\n\t]\n}"
  },
  {
    "path": "commons-jfinger/.gitignore",
    "content": "/target/\n/.classpath\n/.project\n*.iml"
  },
  {
    "path": "commons-jfinger/README.md",
    "content": "\nJFinger 是一个命令行开发框架，可以用 Java 很方便的开发一些基于命令行的功能。\n\n## 目录\n\n* 项目背景\n* JFinger 简介\n* 源码下载\n* 软件版本说明\n* 引入 JFinger 依赖\n* 启用 JFinger\n* JFinger 命令格式\n* JFinger 帮助系统\n* 自己编写命令\n* 资源分享与技术交流\n\n\n## 项目背景\n\n虽然当前软件系统的人机交互是以“图形化”的交互模式为主，但对于“程序猿”这个特殊群体而言，仍非常偏爱“命令行”的交互模式，比如在服务器 OS 市场，占了 70% 以上的市场的 Unix / Linux 系统就是以命令行为主，虽然现代很多 Linux 的版本有图形化模式，但程序员们似乎并不买账，他们绝大多数仍然使用命令行的方式操作着服务器。\n这也说明命令行方式在编程领域有着非常强大的生命力，这或许与它的专业性强、操作简洁、可编程性强的特点是分不开的。\n\n笔者也非常酷爱命令行的操作方式，然而 Linux 上的命令都是针对操作系统功能本身的，不是针对自己开发的具体业务系统的。\n有一次笔者在某 PAAS 平台的项目开发中，根据项目需要编写了一个诊断工具，这个工具以命令行的方式，提供了使用这个 PAAS 平台时的问题诊断功能。\n后来笔者根据这个项目的经验，编写了一套 **命令行开发框架**，可以快速开发基于命令行的工具程序，后来这套“命令行开发框架”就演化成本项目。\n\n## JFinger 简介\n\nJFinger 是一个命令行开发框架，可以用 Java 很方便的开发一些基于命令行的功能。\n本项目取名为“JFinger”， J 代表 Java 的意思， Finger 在英语中是“手指头”的意思，因为敲命令需要动手指头嘛，哈哈！\n\njfinger 是一个 Java 模块，以 jar 包的形式被集成到您的项目中，程序启动后会在控制台提供了命令行方式的交互功能，类似于：\n\n```\n......\n2017-08-23 07:07:34.018  INFO 18300 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)\n2017-08-23 07:07:34.027  INFO 18300 --- [           main] c.t.demo.jfinger.JFingerDemoApplication  : Started JFingerDemoApplication in 4.892 seconds (JVM running for 5.599)\n\nJFinger Command Line Service is starting...\n\njfinger>\n\njfinger>log setLevel -n \"com.terran4j\" -l warn\n成功设置包 com.terran4j 的日志级别为： WARN\n```\n\n也就是说，java程序控制台不再只是单纯输出日志了，而是出现了 `jfinger>` 提示符，您可以在提示符下输入命令，以完成一些与你的程序相关的操作。\n比如上面执行了一条命令 `log setLevel -n \"com.terran4j\" -l warn`，意思是设置 slf4j / log4j 日志级别，将 com.terran4j 包设置为 WARN 级别。\n\n\n## 源码下载\n\nJFinger 现在已开源，欢迎大家使用，源代码放在了 [这里](https://git.oschina.net/terran4j-public/commons/tree/master/commons-jfinger) 。\n\n本文所用的示例代码也放在“码云”上了，欢迎大家免费下载或浏览：\n * [ demo-jfinger ](https://git.oschina.net/terran4j-public/demo/tree/master/demo-jfinger)\n\n\n## 软件版本说明\n\n相关软件使用的版本：\n* Java:  1.8\n* Maven:  3.3.9\n* SpringBoot:  1.5.9.RELEASE\n\n程序在以上版本均调试过，可以正常运行。\n其它版本理论上相同，但仅供参考。\n\n\n## 适用读者\n\n本文适合有Java + Maven + SpringBoot 开发经验的开发者们。\n如果您有 Java 开发经验但对Spring Boot 还不熟悉的话，建议先阅读笔者写过的一本书[ 《Spring Boot 快速入门》 ](http://www.jianshu.com/nb/14688855?order_by=seq)。\n这本书的目标是帮助有 Java 开发经验的程序员们快速掌握使用 Spring Boot 开发的基本技巧，感受到 Spring Boot 的极简开发风格及超爽编程体验。\n\n\n## 引入 JFinger 依赖\n\nJFinger 是笔者（terran4j）多个开源项目的其中一个项目，笔者为了方便大家使用，专门搭建了一个开放的 maven 仓库，并将所有开源项目的 jar 包发布到这个仓库中了，因此需要您在 maven 的 settings.xml 文件上配置上这个仓库，配置方法参见《[配置 terran4j 的 maven 仓库](http://www.jianshu.com/p/283cd7ce3e87)》。\n\n配置好 maven 仓库后，您就可以在您的项目的 pom.xml 文件中引入  JFinger 的依赖了，如：\n\n```xml\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-jfinger</artifactId>\n\t\t\t<version>1.0.5</version>\n\t\t</dependency>\n```\n\nterran4j-commons-jfinger 项目的当前最新稳定本是 1.0.5 ，若有更新升级会本这里给出最新版本号。\n\n整个 pom.xml 文件代码如下所示：\n\n```xml\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<groupId>terran4j</groupId>\n\t<artifactId>demo-jfinger</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t<packaging>jar</packaging>\n\n\t<name>demo-jfinger</name>\n\t<url>http://maven.apache.org</url>\n\n\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>1.5.9.RELEASE</version>\n\t</parent>\n\n\t<properties>\n\t\t<java.version>1.8</java.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-jfinger</artifactId>\n\t\t\t<version>1.0.5</version>\n\t\t</dependency>\n\t</dependencies>\n\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t\t<artifactId>spring-boot-maven-plugin</artifactId>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\n</project>\n```\n\n注意：在 pom.xml 文件的最后面，我们引入了一个 maven 插件 `spring-boot-maven-plugin` 这是帮助我们打出 Spring Boot 独立运行的 jar 包的，Spring Boot 的 jar 包很特别，它将您的项目、以及项目中依赖的所有 jar 包都打在一起，打成一个“胖” jar 包，这样部署起来非常方便，相信用过的同学都能体会到。\n\n\n## 启用 JFinger\n\nJFinger 也是基于 Spring Boot 框架而设计的，要在程序中启用 JFinger 的功能，就必须在 SpringBootApplication 的类上面加上  **@EnableJFinger** 的注解，如下代码所示：\n\n```java\npackage com.terran4j.demo.jfinger;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.jfinger.EnableJFinger;\n\n@EnableJFinger\n@SpringBootApplication\npublic class JFingerDemoApplication {\n\t\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(JFingerDemoApplication.class, args);\n\t}\n\n}\n```\n\n然后我们运行这个 main 函数，控制台输出变成这样了：\n\n```\n......\n2017-08-23 07:07:34.018  INFO 18300 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)\n2017-08-23 07:07:34.027  INFO 18300 --- [           main] c.t.demo.jfinger.JFingerDemoApplication  : Started JFingerDemoApplication in 4.892 seconds (JVM running for 5.599)\n\nJFinger Command Line Service is starting...\n\njfinger>\n```\n\n多了个 jfinger> 的提示符，可以在后面输入命令。\n\n无论您是在 Eclipse IDE 中运行、 还是在 Windows / Linux 中直接用 java -jar xxx.jar 的方式运行，都可以在控制台输入命令。\n不过笔者发现在  Eclipse IDE 中，**输入命令时不能用中文输入法**，似乎是 Eclipse 控制台对中文支持有问题。\n如果一定要在命令中含中文内容的话，建议在 Eclipse 之外把命令写好，再 Ctrl+C, Ctrl+V 拷贝进去执行。\n\n\n## JFinger 命令格式\n\nJFinger 的命令格式为：\n\n```\n【命令组名】 【命令名】 【选项1】 【选项2】 ... 【选项n】\n```\n\n比如上面提到的“设置日志级别”的命令：\n\n```\nlog setLevel -n \"com.terran4j\" -l warn\n```\n\nlog 就是命令组名，setLevel 就是命令名，-n \"com.terran4j\" 及 -l warn 都是这个命令的选项。\n\n一个命令组可以包含多个命令，比如 system 命令组就至少包含两个命令：\n\n```\nsystem prop        显示所有的系统变量的值。\nsystem env         显示所有的环境变量的值。\n```\n\n将命令分组是为了避免命令冲突，在开发命令系统时，不同的团队或不同的模块可以用不同的命令组名。这样命令名就不需要全局唯一，只需要在此命令组内部唯一就可以了。\n\n命令行的选项是由  -<key> <value> 组成，如上面的 -l warn 。\nvalue 值中如果有空格或其它特殊字符，也可以用 \"\" 号包裹起来，如上面的 -n \"com.terran4j\"  \n\n你可以用 help 【命令组名】 【命令名】来查看此命令的选项列表。\n如下所示，输入 help log setLevel 显示出了此命令的详细信息:\n\n```\njfinger>help log setLevel\n命令：  log setLevel [选项]\n选项列表:\n    -n, --loggerName <string>    日志名称，一般是用类全名(不支持通配)。\n    -l, --logLevel <string>      日志级别\n\n说明：  设置日志级别，如： log setLevel -n \"com.terran4j\" -l warn\n```\n\n选项列表中罗列了此命令的所有选项，以这一行为例：\n\n```\n-l, --logLevel <string>      日志级别\n```\n\n-l 表示此选项的 key 为 l ， --logLevel 表示此选项的 name 为 logLevel ，<string> 表示此选项的值是字符串类型的。\n\n选项既可以用 -<key> <value> 来来表示，也可以用 --<name> <value> 来表示，如下面两个命令是等价的：\n\n```\nlog setLevel -n \"com.terran4j\" -l warn\nlog setLevel --loggerName \"com.terran4j\" --logLevel warn\n```\n\n一般来讲，key 非常短（通常用一个字母表示）， 用  -<key> <value> 的写法非常方便；\n而 name 是由完整拼写的单词组成， 用  -<key> <value> 的写法便于理解这个命令选项的意义。\n\n\n## JFinger 帮助系统\n\nJFinger 有一套帮助系统，可以快速了解当前程序中命令的用法。\n这套帮助系统是基于一个特殊的 help 命令提供的，用 help 命令可以查询所有命令的用法。\n\n如果你不知道当前程序中被注入了哪些命令，可以用 help 命令查看，如：\n\n```\njfinger>help\n欢迎使用由 terran4j 提供的命令行服务。\n\n当前程序有以下命令可供使用：\n命令：  spring [profile | prop | showBean]\n说明：  Spring 相关命令，如查看 Spring 中的配置属性等。\n\n命令：  system [env | prop | setProps]\n说明：  系统命令，读取或写入本程序中的系统变量或环境变量等。\n\n命令：  log [setLevel]\n说明：  Logback 日志相关命令，可以用于调整日志级别等操作。\n\n命令：  hello [say]\n说明：  hello命令组，用于演示命令的开发方式。\n\n\n请用 help 查询命令的详细用法，如：\n输入：\n    help\n查看所有的命令组。\n\n输入：\n    help 【groupName】\n查看指定命令组的详细信息，如：\n    help system\n\n输入：\n    help 【groupName】 【commandName】\n查看指定命令的详细信息，如：\n    help system prop\n\n```\n\nhelp 命令罗列了程序内所有的命令的概要信息。\n\n如果你知道某个命令组的名称，但不知道这个命令组中命令的细节，可以用 help 【命令组名】 来查看，如：\n\n```\njfinger>help system\n命令组：  system\n说明：  系统命令，读取或写入本程序中的系统变量或环境变量等。\n命令列表：\n    env: 读取或写入环境变量的值\n    prop: 显示或改写系统系统变量的值，如：\n        system prop        显示所有的系统变量的值。\n        system prop -k abc -v AAA        将 abc 的值改为 123。\n        system prop -k abc        显示此系统变量的值。\n    setProps: 改写系统变量值，如： system setProps -Dk1=123 -Dk2=456\n```\n\n如果你知道某个命令组及命令的名称，但不记得这个命令详细信息（如选项有哪些），可以用 help 【命令组名】 【命令名】来查看，如：\n\n```\njfinger>help system prop\n命令：  system prop [选项]\n选项列表:\n    -k, --key <string>      系统变量的键，不指定则显示所有的系统变量的值。\n    -v, --value <string>    系统变量的值，不指定则表示显示此变量值，指定则表示改写此变量值。\n\n说明：  显示或改写系统系统变量的值，如：\n    system prop        显示所有的系统变量的值。\n    system prop -k abc -v AAA        将 abc 的值改为 123。\n    system prop -k abc        显示此系统变量的值。\n```\n\n\n## 自定义命令\n\n下面，我们来自己编写了一个最简单的命令系统，一个 JFinger 版的 Hello, world 。\n\n首先，我们写一个简单的 Spring Boot 服务类 HelloService ，如下所示：\n\n```java\npackage com.terran4j.demo.jfinger;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class HelloService {\n\n\tprivate final AtomicInteger count = new AtomicInteger(0);\n\n\tpublic final String say(String name) {\n\t\tcount.incrementAndGet();\n\t\treturn \"Hello, \" + name + \"!\";\n\t}\n\n\tpublic final int count() {\n\t\treturn count.get();\n\t}\n\n}\n```\n\n这是一个非常的简单Spring Bean， say 方法返回一句“Hello, xxx”之类的问候句， 而 count 方法显示 say 方法调用的次数。\n\n然后我们要编写一个 HelloCommand 类，让 say 方法和 count 方法可以在命令行中被调用，代码如下：\n\n```java\npackage com.terran4j.demo.jfinger;\n\nimport org.springframework.beans.factory.annotation.Autowired;\n\nimport com.terran4j.commons.jfinger.Command;\nimport com.terran4j.commons.jfinger.CommandGroup;\nimport com.terran4j.commons.jfinger.CommandInterpreter;\nimport com.terran4j.commons.jfinger.CommandOption;\nimport com.terran4j.commons.jfinger.OptionType;\nimport com.terran4j.commons.util.error.BusinessException;\n\n@CommandGroup(desc = \"hello命令组，用于演示命令的开发方式。\")\npublic class HelloCommand {\n\n\t@Autowired\n\tprivate HelloService helloService;\n\n\t@Command( //\n\t\t\tdesc = \"输出问候语，如： \\n\" //\n\t\t\t\t\t+ \"hello say -n world -c 2 -s \\n\" //\n\t\t\t\t\t+ \"将输出（执行多次时序号可能不同）：\\n\" //\n\t\t\t\t\t+ \"1. Hello, world!\" // \n\t\t\t\t\t+ \"2. Hello, world!\", //\n\t\t\toptions = { //\n\t\t\t\t\t@CommandOption(key = \"n\", name = \"name\", required = true, //\n\t\t\t\t\t\t\tdesc = \"目标名称\"), //\n\t\t\t\t\t@CommandOption(key = \"s\", name = \"sequenceNumber\", type = OptionType.BOOLEAN, //\n\t\t\t\t\t\t\tdesc = \"需要序号（序号按调用次数递增）\"), //\n\t\t\t\t\t@CommandOption(key = \"c\", name = \"count\", type = OptionType.INT, //\n\t\t\t\t\t\t\tdesc = \"本次调用多少次\") //\n\t\t\t})\n\tpublic void say(CommandInterpreter ci) throws BusinessException {\n\t\tString name = ci.getOption(\"n\");\n\t\tboolean sequenceNumber = ci.hasOption(\"s\");\n\t\tint count = ci.getOption(\"c\", 1);\n\t\tfor (int i = 0; i < count; i++) {\n\t\t\tString message = helloService.say(name);\n\t\t\tif (sequenceNumber) {\n\t\t\t\tmessage = helloService.count() + \". \" + message;\n\t\t\t}\n\t\t\tci.println(message);\n\t\t}\n\t}\n\n}\n```\n\n说明一下：\n * @CommandGroup 修饰在类上，表示这个类是一个命令组类，其中的 desc 属性是对这个命令组的描述信息（会出现在  help 信息中），\n * 类名建议以   XxxCommand 的方式命名，JFinger 会将 xxx 作为命令组的名称（即 XxxCommand 去掉 Command 然后首字母改为小写）。\n * [@Command](https://my.oschina.net/liuxiliang) 修饰在方法上，表示这个方法是一个命令，方法名就是命令的名称，这个方法**必须有且仅有一个 CommandInterpreter 类型的参数**，且无返回值。\n * 在 [@Command](https://my.oschina.net/liuxiliang) 注解中，**@CommandOption 表示这个方法的选项**。\n\n@CommandGroup 的类定义为：\n\n```\n@Target({TYPE, METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@Component\npublic @interface CommandGroup {\n    ......\n}\n```\n\n表示 @CommandGroup 注解完全具备 @Component 注解的功能，所修饰的类完全可以当一个 Spring Bean 的类。\n所以我们可以用：\n\n```java\n\t@Autowired\n\tprivate HelloService helloService;\n```\n\n来注入 helloService Bean。\n\n写完这个类后，我们重启程序，在 `jfinger>` 提示符下输入 help hello ，结果如下：\n\n```\njfinger>help hello\n命令组：  hello\n说明：  hello命令组，用于演示命令的开发方式。\n命令列表：\n    say: 输出问候语，如： \n        hello say -n world -c 2 -s \n        将输出（执行多次时序号可能不同）：\n        1. Hello, world!\n        2. Hello, world!\n\n``` \n\n这些对命令的解释内容，都是 JFinger 从 HelloCommand 类中提取并自动生成的。\n\n然后我们再执行命令： hello say -n world -c 2 -s ， 结果如下：\n\n```\njfinger>hello say -n world -c 2 -s\n1. Hello, world!\n2. Hello, world!\n```\n\n表示命令成功执行了。\n\n\n## 资源分享与技术交流\n\n如果你觉得本项目对你有用的话，希望可以定期收到更多分享的精彩技术干货，或者希望与笔者交流相关技术问题，可以加一下我们的 **SpringBoot及微服务** 微信公众号（免费的哦），请拿起手机扫描下面的二维码关注下吧！\n![SpringBoot及微服务-公众号二维码](http://upload-images.jianshu.io/upload_images/4489584-f4f91efb322bd92c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)"
  },
  {
    "path": "commons-jfinger/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<parent>\n\t\t<groupId>com.github.terran4j</groupId>\n\t\t<artifactId>terran4j-commons-parent</artifactId>\n\t\t<version>1.0.4-SNAPSHOT</version>\n\t</parent>\n\n\t<artifactId>terran4j-commons-jfinger</artifactId>\n\t<packaging>jar</packaging>\n\t<name>terran4j-commons-jfinger</name>\n\t<url>https://github.com/terran4j/commons</url>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>com.github.terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-util</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<dependency>\n\t\t\t<groupId>commons-cli</groupId>\n\t\t\t<artifactId>commons-cli</artifactId>\n\t\t\t<version>1.3.1</version>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>com.google.code.gson</groupId>\n\t\t\t<artifactId>gson</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/Command.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport static java.lang.annotation.ElementType.METHOD;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target(METHOD)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Command {\n\t\n\t/**\n\t * 命令组名称。\n\t * @return\n\t */\n\tString name() default \"\";\n\t\n\t/**\n\t * 命令组描述。\n\t * @return\n\t */\n\tString desc() default \"\";\n\t\n\t/**\n\t * 命令中的选项。\n\t * @return\n\t */\n\tCommandOption[] options() default {};\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandDefine.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.jfinger.impl.DynMethodCommandExecutor;\n\n/**\n * \n * @author wei.jiang\n */\npublic class CommandDefine {\n\t\n\tprivate final Method method;\n\t\n\tprivate final CommandGroupDefine group;\n\t\n\tprivate final Command command;\n\t\n\tprivate final CommandExecutor executor;\n\t\n\tprivate final String beanName;\n\t\n\tprivate String name;\n\t\n\tprivate String desc;\n\t\n\tprivate final List<CommandOptionDefine> options = new ArrayList<CommandOptionDefine>();\n\n\t/**\n\t * \n\t * @param method\n\t */\n\tpublic CommandDefine(CommandGroupDefine group, Object bean, Method method, String beanName) {\n\t\tsuper();\n\t\tthis.method = method;\n\t\tthis.group = group;\n\t\tthis.beanName = beanName;\n\t\tthis.command = this.method.getAnnotation(Command.class);\n\t\tthis.executor = new DynMethodCommandExecutor(bean, method, this.beanName);\n\t\t\n\t\tString name = command.name();\n\t\tif (StringUtils.isEmpty(name)) {\n\t\t\tname = method.getName();\n\t\t\tsetName(name);\n\t\t}\n\t\t\n\t\tsetDesc(command.desc());\n\t\t\n\t\tCommandOption[] commandOptions = command.options();\n\t\tif (commandOptions != null) {\n\t\t\tfor (CommandOption commandOption : commandOptions) {\n\t\t\t\tCommandOptionDefine option = new CommandOptionDefine(commandOption);\n\t\t\t\taddOption(option);\n\t\t\t}\n\t\t}\n\t}\n\t\n\tpublic void addOption(CommandOptionDefine option) {\n\t\tthis.options.add(option);\n\t}\n\t\n\tpublic void addOptions(CommandOptionDefine[] options) {\n\t\tthis.options.addAll(Arrays.asList(options));\n\t}\n\t\n\tpublic void addOptions(List<CommandOptionDefine> options) {\n\t\tthis.options.addAll(options);\n\t}\n\t\n\t/**\n\t * @return the name\n\t */\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name the name to set\n\t */\n\tpublic final void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return the executor\n\t */\n\tpublic final CommandExecutor getExecutor() {\n\t\treturn executor;\n\t}\n\n\t/**\n\t * @return the options\n\t */\n\tpublic final List<CommandOptionDefine> getOptions() {\n\t\treturn options;\n\t}\n\t\n\tpublic final CommandOptionDefine getOption(String key) {\n\t\tfor (CommandOptionDefine option : options) {\n\t\t\tif (option.getKey().equals(key)) {\n\t\t\t\treturn option;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * @return the CommandGroupDefine\n\t */\n\tpublic final CommandGroupDefine getGroup() {\n\t\treturn group;\n\t}\n\t\n\tpublic String getHelp() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(\"命令：  \").append(group.getName()).append(\" \").append(name).append(\" [选项]\");\n\t\t\n\t\tif (options.size() > 0) {\n\t\t\tsb.append(\"\\n选项列表:\\n\");\n\t\t\t\n\t\t\t// 找出选项占位最长值。\n\t\t\tint max = -1;\n\t\t\tint[] optSizes = new int[options.size()];\n\t\t\tfor (int i = 0; i < optSizes.length; i++) {\n\t\t\t\tCommandOptionDefine option = options.get(i);\n\t\t\t\toptSizes[i] = option.getKey().length();\n\t\t\t\t\n\t\t\t\tString name = option.getName();\n\t\t\t\tif (!StringUtils.isEmpty(name)) {\n\t\t\t\t\toptSizes[i] += (4 + name.length());\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tOptionType type = option.getType();\n\t\t\t\tif (type != OptionType.BOOLEAN) {\n\t\t\t\t\toptSizes[i] += (3 + type.getName().length());\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (max < optSizes[i]) {\n\t\t\t\t\tmax = optSizes[i];\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\tfor (int i = 0; i < optSizes.length; i++) {\n\t\t\t\tCommandOptionDefine option = options.get(i);\n\t\t\t\t\n\t\t\t\tString key = option.getKey();\n\t\t\t\tsb.append(\"    \").append(\"-\").append(key);\n\t\t\t\t\n\t\t\t\tString name = option.getName();\n\t\t\t\tif (!StringUtils.isEmpty(name)) {\n\t\t\t\t\tsb.append(\", --\").append(name);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tOptionType type = option.getType();\n\t\t\t\tif (type != OptionType.BOOLEAN) {\n\t\t\t\t\tsb.append(\" <\").append(type.getName()).append(\">\");\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tString desc = option.getDesc();\n\t\t\t\tif (!StringUtils.isEmpty(desc)) {\n\t\t\t\t\t\n\t\t\t\t\t// 补齐不同选项中的空间。\n\t\t\t\t\tint left = max - optSizes[i] + 4;\n\t\t\t\t\tfor (int j = 0; j < left; j++) {\n\t\t\t\t\t\tsb.append(\" \");\n\t\t\t\t\t}\n\t\t\t\t\tsb.append(desc);\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tsb.append(\"\\n\");\n\t\t\t}\n\t\t}\n\t\t\n\t\tString commandDesc = getDesc();\n\t\tif (StringUtils.hasText(commandDesc)) {\n\t\t\tcommandDesc = commandDesc.replaceAll(\"\\n\", \"\\n    \");\n\t\t\tsb.append(\"\\n说明：  \").append(commandDesc);\n\t\t}\n\t\t\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * @return the desc\n\t */\n\tpublic final String getDesc() {\n\t\treturn desc;\n\t}\n\n\t/**\n\t * @param desc the desc to set\n\t */\n\tpublic final void setDesc(String desc) {\n\t\tthis.desc = desc;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandErrorCode.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport com.terran4j.commons.util.error.ErrorCode;\n\npublic enum CommandErrorCode implements ErrorCode {\n\t\n\t// Common Error.\n\tCOMMAND_FORMAT_INVALID(1000510, \"command.format.invalid\"),\n\tARG_QUOTE_NOT_CLOSE(1000511, \"arg.quote.not.close\"),\n\tCOMMAND_NOT_FOUND_BY_GROUP(1000511, \"command.not.found.by.group\"),\n\tCOMMAND_NOT_FOUND_BY_NAME(1000512, \"command.not.found.by.name\"),\n\tOPTION_KEY_EMPTY(1000513, \"command.option.key.is.empty\"),\n\tARGS_PARSE_ERROR(1000514, \"args.parse.error\"),\n\tGET_EXECUTOR_ERROR(1000515, \"get.command.executor.error\"),\n\tCOMMAND_CLASS_EMPTY(1000516, \"command.class.empty\"),\n\tCOMMAND_EXECUTE_ERROR(1000517, \"command.execute.error\"),\n\tARG_PARSE_TO_INT_ERROR(1000518, \"arg.parse.to.int.error\"),\n\tCOMMAND_DEFINE_ERROR(1000519, \"command.define.error\"),\n\tUNKNOW_OPTION_TYPE(1000520, \"unknow.option.type\"),\n\tCOMMAND_SERVICE_NOT_FOUND(1000521, \"command.service.not.found\"),\n\t\n\t// \n\tCOMMAND_NAME_NOT_FOUND(1000501, \"command.name.not.found\"),\n\tCOMMAND_NAME_DUPLICATED(1000502, \"command.name.duplicated\"),\n\t\n\t;\n\t\n\tprivate static final Set<Integer> codes = new HashSet<Integer>();\n\t\n\tstatic {\n\t\tErrorCode[] array = values();\n\t\tfor (ErrorCode code : array) {\n\t\t\tcodes.add(code.getValue());\n\t\t}\n\t}\n\t\n\tpublic static final boolean contains(ErrorCode code) {\n\t\tif (code == null) {\n\t\t\treturn false;\n\t\t}\n\t\treturn codes.contains(code.getValue());\n\t}\n\n\tprivate final int value;\n\t\n\tprivate final String name;\n\n\tprivate CommandErrorCode(int value, String name) {\n\t\tthis.value = value;\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return the value\n\t */\n\tpublic final int getValue() {\n\t\treturn value;\n\t}\n\t\n\t/**\n\t * @return the name\n\t */\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandException.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport com.terran4j.commons.util.error.BusinessException;\n\npublic class CommandException extends BusinessException {\n\n\tprivate static final long serialVersionUID = -691993852577468850L;\n\n\tpublic CommandException(String code) {\n\t\tsuper(code);\n\t}\n\n\tpublic CommandException(String code, Throwable cause) {\n\t\tsuper(code, cause);\n\t}\n\t\n\t\n\n}"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandExecutor.java",
    "content": "package com.terran4j.commons.jfinger;\n\n/**\n * \n * @author jiangwei \n */\npublic interface CommandExecutor {\n\t\n\tvoid execute(CommandInterpreter ci);\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandGroup.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport static java.lang.annotation.ElementType.METHOD;\nimport static java.lang.annotation.ElementType.TYPE;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.springframework.stereotype.Component;\n\n@Target({TYPE, METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@Component\npublic @interface CommandGroup {\n\n\t/**\n\t * 命令组名称。\n\t * @return\n\t */\n\tString name() default \"\";\n\t\n\t/**\n\t * 命令组描述。\n\t * @return\n\t */\n\tString desc() default \"\";\n\t\n\t/**\n\t * 公共的命令行选项。\n\t * @return\n\t */\n\tCommandOption[] options() default {};\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandGroupDefine.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * \n * @author wei.jiang\n */\npublic class CommandGroupDefine {\n\n\tprivate String name;\n\t\n\tprivate String desc;\n\t\n\tprivate List<CommandDefine> commandsList = new ArrayList<CommandDefine>();\n\t\n\tprivate Map<String, CommandDefine> commandsMap = new HashMap<String, CommandDefine>();\n\t\n\tpublic CommandGroupDefine() {\n\t\tsuper();\n\t}\n\t\n\tpublic CommandGroupDefine(CommandGroup anno) {\n\t\tsuper();\n\t\tthis.name = anno.name();\n\t\tthis.desc = anno.desc();\n\t}\n\n\tpublic List<CommandDefine> getCommands() {\n\t\treturn commandsList;\n\t}\n\t\n\tpublic int size() {\n\t\treturn commandsList.size();\n\t}\n\t\n\tpublic void addCommand(CommandDefine commandDefine) {\n\t\tcommandsList.add(commandDefine);\n\t\tString key = commandDefine.getName();\n\t\tcommandsMap.put(key, commandDefine);\n\t}\n\t\n\tpublic CommandDefine getCommand(String name) {\n\t\treturn commandsMap.get(name);\n\t}\n\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\tpublic void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\tpublic String getDesc() {\n\t\treturn desc;\n\t}\n\n\tpublic void setDesc(String desc) {\n\t\tthis.desc = desc;\n\t}\n\n\t\n\t\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandGroups.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class CommandGroups {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(CommandGroups.class);\n\n\tprivate final Map<String, CommandGroupDefine> commands = new HashMap<String, CommandGroupDefine>();\n\t\n\tpublic void addCommandGroup(CommandGroupDefine group) {\n\t\tif (group == null) {\n\t\t\tthrow new NullPointerException(\"group is null.\");\n\t\t}\n\t\t\n\t\tString groupName = group.getName();\n\t\tif (commands.containsKey(groupName)) {\n\t\t\tif (log.isWarnEnabled()) {\n\t\t\t\tCommandGroupDefine existed = commands.get(groupName);\n\t\t\t\tlog.warn(\"CommandGroup[{}] is duplicated with another one[{}]\", group, existed);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tthis.commands.put(groupName, group);\n\t}\n\n\tpublic boolean containsKey(Object key) {\n\t\treturn commands.containsKey(key);\n\t}\n\n\tpublic CommandGroupDefine get(Object key) {\n\t\treturn commands.get(key);\n\t}\n\n\tpublic CommandGroupDefine put(String key, CommandGroupDefine value) {\n\t\treturn commands.put(key, value);\n\t}\n\n\tpublic Set<String> keySet() {\n\t\treturn commands.keySet();\n\t}\n\t\n\t\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandInterpreter.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport java.util.Properties;\n\nimport com.terran4j.commons.util.error.BusinessException;\n\npublic interface CommandInterpreter {\n\n\tboolean hasOption(String key);\n\n\tint getOption(String key, int defaultValue) throws BusinessException;\n\n\tString getOption(String key);\n\n\tString getOption(String key, String defaultValue);\n\n\tProperties getOption(String key, Properties defaultValue);\n\n\tvoid print(String msg);\n\n\tvoid println(String msg);\n\n\tvoid println(String msg, Object... args);\n\n\tvoid println();\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandOption.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\n@Target({})\n@Retention(RUNTIME)\npublic @interface CommandOption {\n\n\t/**\n\t * \n\t * @return\n\t */\n\tString key();\n\n\tString name() default \"\";\n\n\tString desc() default \"\";\n\t\n\tboolean required() default false;\n\t\n\tOptionType type() default OptionType.STRING;\n}"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandOptionDefine.java",
    "content": "package com.terran4j.commons.jfinger;\n\npublic class CommandOptionDefine {\n\n\tprivate final OptionType type;\n\n\tprivate final String key;\n\t\n\tprivate final boolean required;\n\n\tprivate final String name;\n\t\n\tprivate final String desc;\n\t\n\tpublic CommandOptionDefine(CommandOption option) {\n\t\tsuper();\n\t\tthis.key = option.key();\n\t\tthis.required = option.required();\n\t\tthis.name = option.name();\n\t\tthis.desc = option.desc();\n\t\tthis.type = option.type();\n\t}\n\n\t/**\n\t * @return the type\n\t */\n\tpublic final OptionType getType() {\n\t\treturn type;\n\t}\n\n\t/**\n\t * @return the key\n\t */\n\tpublic final String getKey() {\n\t\treturn key;\n\t}\n\n\t/**\n\t * @return the required\n\t */\n\tpublic final boolean isRequired() {\n\t\treturn required;\n\t}\n\n\t/**\n\t * @return the name\n\t */\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @return the desc\n\t */\n\tpublic final String getDesc() {\n\t\treturn desc;\n\t}\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/CommandOptionType.java",
    "content": "package com.terran4j.commons.jfinger;\n\n/**\n * 标识一个命令参数的类型。\n * @author jiangwei\n */\npublic enum CommandOptionType {\n\n\tSTRING, // \n\t\n\tBOOLEAN, // \n\t\n\tINT, // \n\t\n\tPROPERTIES;\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/EnableJFinger.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.springframework.context.annotation.Import;\n\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@Import(JFingerConfiguration.class)\npublic @interface EnableJFinger {\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/Encoding.java",
    "content": "package com.terran4j.commons.jfinger;\n\npublic enum Encoding {\n\n\tUTF8(\"UTF-8\"), GBK(\"GBK\");\n\t\n\tprivate String name;\n\t\n\tprivate Encoding(String name){\n\t\tthis.name = name;\n\t}\n\t\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\t\n\tpublic static Encoding getDefaultEncoding(){\n\t\treturn UTF8;\n\t}\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/JFingerConfiguration.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\n\nimport com.terran4j.commons.jfinger.builtin.LogCommand;\nimport com.terran4j.commons.jfinger.impl.CommandLineApplicationListener;\n\n/**\n * 标记本包下的 bean 自动配置。\n * \n * @author wei.jiang\n */\n@ComponentScan(basePackageClasses = { //\n\t\tCommandLineApplicationListener.class, //\n\t\tLogCommand.class //\n})\n@Configuration\npublic class JFingerConfiguration {\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/OptionType.java",
    "content": "package com.terran4j.commons.jfinger;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic enum OptionType {\n\n\tBOOLEAN(\"boolean\"), STRING(\"string\"), INT(\"int\"), PROPERTIES(\"properties\");\n\n\tprivate static final Map<String, OptionType> values = new HashMap<String, OptionType>();\n\n\tstatic {\n\t\tOptionType[] array = OptionType.values();\n\t\tfor (OptionType type : array) {\n\t\t\tvalues.put(type.getName(), type);\n\t\t}\n\t}\n\n\tpublic static final OptionType toValue(String name) {\n\t\tif (name == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn values.get(name);\n\t}\n\n\tprivate String name;\n\n\tprivate OptionType(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return the name\n\t */\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n}"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/builtin/LogCommand.java",
    "content": "package com.terran4j.commons.jfinger.builtin;\n\nimport org.springframework.boot.logging.LogLevel;\nimport org.springframework.boot.logging.logback.LogbackLoggingSystem;\n\nimport com.terran4j.commons.jfinger.Command;\nimport com.terran4j.commons.jfinger.CommandGroup;\nimport com.terran4j.commons.jfinger.CommandInterpreter;\nimport com.terran4j.commons.jfinger.CommandOption;\n\n@CommandGroup(options = {}, desc = \"Logback 日志相关命令，可以用于调整日志级别等操作。\")\npublic class LogCommand {\n\n\t@Command(desc = \"设置日志级别，如： log setLevel -n \\\"com.terran4j\\\" -l warn \", options = { //\n\t\t\t@CommandOption(key = \"n\", name = \"loggerName\", required = true, desc = \"日志名称，一般是用类全名(不支持通配)。\"), //\n\t\t\t@CommandOption(key = \"l\", name = \"logLevel\", required = true, desc = \"日志级别\") //\n\t})\n\tpublic void setLevel(CommandInterpreter ci) {\n\t\tString loggerName = ci.getOption(\"n\");\n\t\tString logLevel = ci.getOption(\"l\");\n\t\tlogLevel = logLevel.trim().toUpperCase();\n\t\tLogLevel level = LogLevel.valueOf(logLevel);\n\t\tif (level == null) {\n\t\t\tci.println(\"不能识别的logLevel： \" + logLevel);\n\t\t\treturn;\n\t\t}\n\n\t\tClassLoader classLoader = Thread.currentThread().getContextClassLoader();\n\t\tLogbackLoggingSystem logbackLoggingSystem = new LogbackLoggingSystem(classLoader);\n\t\tlogbackLoggingSystem.setLogLevel(loggerName, level);\n\t\tci.println(\"成功设置包 {} 的日志级别为： {} \", loggerName, logLevel);\n\t}\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/builtin/SpringCommand.java",
    "content": "package com.terran4j.commons.jfinger.builtin;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.jfinger.Command;\nimport com.terran4j.commons.jfinger.CommandGroup;\nimport com.terran4j.commons.jfinger.CommandInterpreter;\nimport com.terran4j.commons.jfinger.CommandOption;\nimport com.terran4j.commons.jfinger.OptionType;\n\n@CommandGroup(options = {}, desc = \"Spring 相关命令，如查看 Spring 中的配置属性等。\")\npublic class SpringCommand implements ApplicationContextAware {\n\n\tprivate ApplicationContext context = null;\n\n\t@Override\n\tpublic void setApplicationContext(ApplicationContext context) throws BeansException {\n\t\tthis.context = context;\n\t}\n\n\t@Command( //\n\t\t\tdesc = \"查看spring中配置属性的值，如：\\n\" //\n\t\t\t\t\t+ \"spring prop -k \\\"abc\\\"        查看 Spring 容器中 key 为 abc 的属性值\", //\n\t\t\toptions = { //\n\t\t\t\t\t@CommandOption(key = \"k\", name = \"key\", required = true, desc = \"属性的键\") //\n\t\t\t})\n\tpublic void prop(CommandInterpreter ci) {\n\t\tString propKey = ci.getOption(\"k\");\n\t\tString propValue = context.getEnvironment().getProperty(propKey);\n\t\tci.println(propValue);\n\t}\n\n\t@Command( //\n\t\t\tdesc = \"查看spring中的 profile，如：\\n\" //\n\t\t\t\t\t+ \"spring profile -a        查看 Spring 容器中活跃的 profile 。\", //\n\t\t\toptions = { //\n\t\t\t\t\t@CommandOption(key = \"a\", name = \"onlyActive\", type = OptionType.BOOLEAN, //\n\t\t\t\t\t\t\tdesc = \"是否只看活跃的 profile\") //\n\t\t\t})\n\tpublic void profile(CommandInterpreter ci) {\n\t\tboolean onlyActive = ci.hasOption(\"a\");\n\t\tString[] profiles = null;\n\t\tif (onlyActive) {\n\t\t\tprofiles = context.getEnvironment().getActiveProfiles();\n\t\t} else {\n\t\t\tprofiles = context.getEnvironment().getDefaultProfiles();\n\t\t}\n\t\tif (profiles == null) {\n\t\t\tci.println(\"null\");\n\t\t} if (profiles.length == 0) {\n\t\t\tci.println(\"[]\");\n\t\t} else {\n\t\t\tfor (String profile : profiles) {\n\t\t\t\tci.println(profile);\n\t\t\t}\n\t\t}\n\t}\n\t\n\t@Command( //\n\t\t\tdesc = \"查看 Spring 容器中的 Bean 对象，如：\\n\" //\n\t\t\t\t\t+ \"spring showBean        列出所有的 Bean 的名称。\\n\"\n\t\t\t\t\t+ \"spring showBean -n \\\"Command\\\"        列出指定名称的 Bean 信息。\", //\n\t\t\toptions = { //\n\t\t\t\t\t@CommandOption(key = \"n\", name = \"name\", desc = \"根据 Bean 名称精确搜索\"), //\n//\t\t\t\t\t@CommandOption(key = \"p\", name = \"pattern\", desc = \"正则表达式，根据 Bean 的名称模糊搜索。\") //\n\t\t\t})\n\tpublic void showBean(CommandInterpreter ci) {\n\t\tString[] beanNames = context.getBeanDefinitionNames();\n\t\t\n\t\tString name = ci.getOption(\"n\");\n\t\tif (StringUtils.hasText(name)) {\n\t\t\tname = name.trim();\n\t\t\tfor (String beanName : beanNames) {\n\t\t\t\tif (beanName.contains(name.subSequence(0, name.length()))) {\n\t\t\t\t\tci.println(beanName);\t\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n//\t\tString patternText = ci.getOption(\"p\");\n//\t\tif (StringUtils.hasText(patternText)) {\n//\t\t\tpatternText = patternText.trim();\n//\t\t\tPattern pattern = Pattern.compile(patternText);\n//\t\t\tfor (String beanName : beanNames) {\n//\t\t\t\tif (pattern.matches(arg0, arg1)) {\n//\t\t\t\t\tci.println(beanName);\n//\t\t\t\t}\n//\t\t\t}\n//\t\t\treturn;\n//\t\t}\n\t\t\n\t\tfor (String beanName : beanNames) {\n\t\t\tci.println(beanName);\n\t\t}\n\t}\n\n\t// @Command(desc = \"查看spring中配置属性的值\", options = { //\n\t// @CommandOption(key = \"k\", name = \"keyWord\", desc = \"属性的键\") //\n\t// })\n\t// public void searchBean(CommandInterpreter ci) {\n\t// String keyWord = ci.getOption(\"k\");\n\t// String[] beanNames = context.getBeanDefinitionNames();\n\t// }\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/builtin/SystemCommand.java",
    "content": "package com.terran4j.commons.jfinger.builtin;\n\nimport java.util.Iterator;\nimport java.util.Properties;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.jfinger.Command;\nimport com.terran4j.commons.jfinger.CommandGroup;\nimport com.terran4j.commons.jfinger.CommandInterpreter;\nimport com.terran4j.commons.jfinger.CommandOption;\nimport com.terran4j.commons.jfinger.OptionType;\n\n@CommandGroup(options = {}, desc = \"系统命令，读取或写入本程序中的系统变量或环境变量等。\")\npublic class SystemCommand {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(SystemCommand.class);\n\n\t@Command(desc = \"改写系统变量值，如： system setProps -Dk1=123 -Dk2=456\", options = {\n\t\t\t@CommandOption(key = \"D\", type = OptionType.PROPERTIES) })\n\tpublic void setProps(CommandInterpreter ci) {\n\t\tProperties props = ci.getOption(\"D\", new Properties());\n\t\tSystem.setProperties(props);\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"setProps done: {}\", props);\n\t\t}\n\t}\n\n\t@Command(desc = \"显示或改写系统系统变量的值，如：\\n\" //\n\t\t\t+ \"system prop        显示所有的系统变量的值。\\n\" //\n\t\t\t+ \"system prop -k abc -v AAA        将 abc 的值改为 123。\\n\" //\n\t\t\t+ \"system prop -k abc        显示此系统变量的值。\", //\n\t\t\toptions = { //\n\t\t\t\t\t@CommandOption(key = \"k\", name = \"key\", //\n\t\t\t\t\t\t\tdesc = \"系统变量的键，不指定则显示所有的系统变量的值。\"), //\n\t\t\t\t\t@CommandOption(key = \"v\", name = \"value\", //\n\t\t\t\t\t\t\tdesc = \"系统变量的值，不指定则表示显示此变量值，指定则表示改写此变量值。\") })\n\tpublic void prop(CommandInterpreter ci) {\n\t\tif (log.isDebugEnabled()) {\n\t\t\tlog.debug(\"system prop\");\n\t\t}\n\n\t\tString key = ci.getOption(\"k\");\n\t\tif (log.isDebugEnabled()) {\n\t\t\tlog.debug(\"key = {}\", key);\n\t\t}\n\n\t\t// 不指定key，则打印所有的系统变量\n\t\tif (StringUtils.isEmpty(key)) {\n\t\t\tif (log.isDebugEnabled()) {\n\t\t\t\tlog.debug(\"key is empty, will print all properties.\");\n\t\t\t}\n\t\t\tIterator<Object> it = System.getProperties().keySet().iterator();\n\t\t\tci.println(\"all System Properties:\");\n\t\t\twhile (it.hasNext()) {\n\t\t\t\tObject propKey = it.next();\n\t\t\t\tif (propKey instanceof String) {\n\t\t\t\t\tString propKeyStr = (String) propKey;\n\t\t\t\t\tObject propValue = System.getProperty(propKeyStr);\n\t\t\t\t\tci.println(propKeyStr + \" = \" + propValue);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tString value = ci.getOption(\"v\");\n\t\tif (StringUtils.isEmpty(value)) {\n\t\t\tString propValue = System.getProperty(key);\n\t\t\tci.println(key + \" = \" + propValue);\n\t\t} else {\n\t\t\tString propValueOld = System.getProperty(key);\n\t\t\tci.println(key + \" = \" + propValueOld);\n\t\t\tSystem.setProperty(key, value);\n\t\t\tString propValueNew = System.getProperty(key);\n\t\t\tci.println(\"System setProperty done.\");\n\t\t\tci.println(key + \" = \" + propValueNew);\n\t\t}\n\t}\n\n\t@Command(desc = \"读取或写入环境变量的值\", options = { //\n\t\t\t@CommandOption(key = \"k\", name = \"key\", desc = \"环境变量的键，不指定则打印所有的环境变量\") //\n\t})\n\tpublic void env(CommandInterpreter ci) {\n\t\tString key = ci.getOption(\"k\");\n\t\tif (log.isDebugEnabled()) {\n\t\t\tlog.debug(\"key = {}\", key);\n\t\t}\n\n\t\t// 不指定key，则打印所有的环境变量\n\t\tif (StringUtils.isEmpty(key)) {\n\t\t\tif (log.isDebugEnabled()) {\n\t\t\t\tlog.debug(\"key is empty, will print all env vars.\");\n\t\t\t}\n\t\t\tIterator<String> it = System.getenv().keySet().iterator();\n\t\t\tci.println(\"all Env Vars:\");\n\t\t\twhile (it.hasNext()) {\n\t\t\t\tString envKey = it.next();\n\t\t\t\tObject envValue = System.getenv(envKey);\n\t\t\t\tci.println(envKey + \" = \" + envValue);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tString envValue = System.getenv(key);\n\t\tci.println(key + \" = \" + envValue);\n\t}\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/impl/BackCommandException.java",
    "content": "package com.terran4j.commons.jfinger.impl;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.MissingResourceException;\nimport java.util.ResourceBundle;\nimport java.util.Set;\nimport java.util.Stack;\n\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.jfinger.impl.Util;\nimport com.terran4j.commons.util.error.ErrorCode;\n\n/**\n * 异常基类。\n * TODO: extends BussinessException\n * @author jiangwei\n */\npublic class BackCommandException extends RuntimeException {\n\t\n\t/**\n\t * serialVersionUID\n\t */\n\tprivate static final long serialVersionUID = 5988465338967853686L;\n\t\n\t/**\n\t * \n\t */\n\tprivate static final Object NULL = new Object();\n\t\n\t/**\n\t * \n\t */\n\tprivate static final Util util = new Util();\n\t\n\t/**\n\t * \n\t */\n\tprivate static ResourceBundle bundle = null;\n\t\n\tprivate static final class ErrorInfo {\n\t\t\n\t\tfinal Map<String, Object> infos = new HashMap<String, Object>();\n\t\t\n\t\tfinal String message;\n\n\t\tpublic ErrorInfo(String message) {\n\t\t\tsuper();\n\t\t\tthis.message = message;\n\t\t}\n\t\t\n\t\t/**\n\t\t * @return the message\n\t\t */\n\t\tpublic final String getMessage() {\n\t\t\treturn message;\n\t\t}\n\t\t\n\t\tpublic void put(String key, Object value) {\n\t\t\tif (value == null) {\n\t\t\t\tvalue = NULL;\n\t\t\t}\n\t\t\tinfos.put(key, value);\n\t\t}\n\t\t\n\t\tpublic Object get(String key) {\n\t\t\tObject value = infos.get(key);\n\t\t\tif (value == NULL) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\t}\n\t\n\t/**\n\t * \n\t * @return\n\t */\n\tprivate static final ResourceBundle getBundle() {\n\t\tif (bundle != null) {\n\t\t\treturn bundle;\n\t\t}\n\t\tString path = \"command.error.properties\";\n\t\tbundle = ResourceBundle.getBundle(path);\n\t\treturn bundle;\n\t}\n\t\n\tprivate final Stack<ErrorInfo> infos = new Stack<BackCommandException.ErrorInfo>();\n\t\n\tprivate final ErrorCode code;\n\n\tpublic BackCommandException(ErrorCode code) {\n\t\tsuper(\"Error Code: \" + code.getValue());\n\t\tthis.code = code;\n\t}\n\t\n\tpublic BackCommandException(ErrorCode code, String message) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n\t\n\tpublic BackCommandException(ErrorCode code, Throwable cause) {\n\t\tsuper(cause);\n\t\tthis.code = code;\n\t}\n\t\n\tpublic BackCommandException(ErrorCode code, String message, Throwable cause) {\n\t\tsuper(message, cause);\n\t\tthis.code = code;\n\t}\n\t\n\tpublic final BackCommandException push(String msg) {\n\t\tthis.infos.push(new ErrorInfo(msg));\n\t\treturn this;\n\t}\n\t\n\tpublic final BackCommandException setInfo(String key, Object value) {\n\t\tif (infos.size() > 0) {\n\t\t\tinfos.peek().put(key, value);\n\t\t} else {\n\t\t\tinfos.push(new ErrorInfo(super.getMessage())).put(key, value);\n\t\t}\n\t\treturn this;\n\t}\n\t\n\tpublic final Object getInfo(String key) {\n\t\tif (infos.size() == 0) {\n\t\t\treturn null;\n\t\t}\n\t\treturn infos.peek().get(key);\n\t}\n\t\n\tpublic static final BackCommandException wrap(ErrorCode code) {\n\t\treturn new BackCommandException(code);\n\t}\n\t\n\tpublic static final BackCommandException wrap(ErrorCode code, String msg) {\n\t\treturn new BackCommandException(code, msg);\n\t}\n\t\n\tpublic static final BackCommandException wrap(ErrorCode code, Throwable t) {\n\t\tif (t instanceof BackCommandException) {\n\t\t\tBackCommandException be = (BackCommandException) t;\n\t\t\tif (be.getErrorCode() == code) {\n\t\t\t\treturn be.push(t.getMessage());\n\t\t\t}\n\t\t}\n\t\treturn new BackCommandException(code, t);\n\t}\n\t\n\tpublic static final BackCommandException wrap(ErrorCode code, Throwable t, String msg) {\n\t\tif (t instanceof BackCommandException) {\n\t\t\tBackCommandException be = (BackCommandException) t;\n\t\t\tif (be.getErrorCode() == code) {\n\t\t\t\treturn be.push(msg);\n\t\t\t}\n\t\t}\n\t\treturn new BackCommandException(code, msg, t);\n\t}\n\t\n\tpublic ErrorCode getErrorCode() {\n\t\treturn code;\n\t}\n\t\n\tprivate String getResource(String key) {\n\t\tif (StringUtils.isEmpty(key)) {\n\t\t\treturn null;\n\t\t}\n\t\tkey = key.trim();\n\t\t\n\t\tString message = null;\n\t\ttry {\n\t\t\tmessage = getBundle().getString(key);\n\t\t} catch (MissingResourceException e) {\n\t\t\t// ignore it.\n\t\t}\n\t\tif (StringUtils.isEmpty(message)) {\n\t\t\tmessage = key.replace('.', ' ');\n\t\t}\n\t\treturn message;\n\t}\n\t\n\t/**\n\t * \n\t * @param element \n\t * @return \n\t */\n\tprivate String toString(StackTraceElement element) {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(element.getClassName()).append(\".\");\n\t\tsb.append(element.getMethodName());\n\t\tsb.append(\"(\").append(element.getFileName());\n\t\tsb.append(\":\").append(element.getLineNumber()).append(\")\");\n\t\treturn sb.toString();\n\t}\n\t\n\t@Override\n\tpublic String getMessage() {\n\t\treturn getReport();\n\t}\n\t\n\t/**\n\t * 获取异常的简要信息。\n\t * @return 异常简要信息。\n\t */\n\tpublic String getReport() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\t\n\t\tThrowable t = this;\n\t\twhile (t != null) {\n\t\t\tsb.append(\"\\n---------------- Exception Stack ----------------\\n\");\n\t\t\tStackTraceElement[] stackTrace = t.getStackTrace();\n\t\t\tif (t instanceof BackCommandException) {\n\t\t\t\tBackCommandException backCommandException = (BackCommandException) t;\n\t\t\t\tsb.append(toString(stackTrace[1])).append(\"\\n\");\n\t\t\t\tbackCommandException.putDetails(sb);\n\t\t\t} else {\n\t\t\t\tsb.append(toString(stackTrace[0])).append(\"\\n\");\n\t\t\t\tsb.append(util.getString(t));\n\t\t\t}\n\t\t\tt = t.getCause();\n\t\t}\n\t\tsb.append(\"\\n\");\n\t\t\n\t\treturn sb.toString();\n\t}\n\t\n\t/**\n\t * 获取异常的描述信息。\n\t * @return\n\t */\n\tpublic String getDescription() {\n\t\treturn getResource(code.getName());\n\t}\n\t\n\t/**\n\t * \n\t * @param sb\n\t */\n\tprivate void putDetails(StringBuilder sb) {\n\t\tsb.append(getClass().getName());\n\t\tsb.append(\": Code = \").append(code.getValue()).append(\", \");\n\t\tsb.append(getDescription()).append(\"\\n\");\n\t\twhile (!infos.empty()) {\n\t\t\tsb.append(\"    ------------ Error Code Stack ------------\\n\");\n\t\t\tErrorInfo error = infos.pop();\n\t\t\tsb.append(\"    Cause by: \").append(error.getMessage()).append(\"\\n\");\n\t\t\tSet<String> keys = error.infos.keySet();\n\t\t\tif (keys != null && keys.size() > 0) {\n\t\t\t\tsb.append(\"    More Detail Information:\\n\");\n\t\t\t\tfor (String key : keys) {\n\t\t\t\t\tObject value = error.infos.get(key);\n\t\t\t\t\tsb.append(\"    \").append(key).append(\": \");\n\t\t\t\t\tif (value != null && value != NULL) {\n\t\t\t\t\t\tString valueText = getString(value);\n\t\t\t\t\t\tsb.append(valueText);\n\t\t\t\t\t}\n\t\t\t\t\tsb.append(\"\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t/**\n\t * 获取对象的描述信息。\n\t * @param value 具体对象。\n\t * @return 对象的字符串信息。\n\t */\n\tprivate String getString(Object value) {\n\t\tif (value == null) {\n\t\t\treturn \"\";\n\t\t}\n\t\t\n\t\tif (value.getClass().isArray()) {\n\t\t\tStringBuilder sb = new StringBuilder(\"[\");\n\t\t\tboolean first = true;\n\t\t\tObject[] array = (Object[])value;\n\t\t\tfor (Object item : array) {\n\t\t\t\tif (!first) {\n\t\t\t\t\tsb.append(\", \");\n\t\t\t\t}\n\t\t\t\tfirst = false;\n\t\t\t\tsb.append(getString(item));\n\t\t\t}\n\t\t\tsb.append(\"]\");\n\t\t\treturn sb.toString();\n\t\t}\n\t\t\n\t\treturn value.toString();\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/impl/CommandInterpreterImpl.java",
    "content": "package com.terran4j.commons.jfinger.impl;\n\nimport com.terran4j.commons.jfinger.*;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.apache.commons.cli.CommandLine;\nimport org.springframework.util.StringUtils;\n\nimport java.io.PrintStream;\nimport java.util.Properties;\n\npublic class CommandInterpreterImpl implements CommandInterpreter {\n\n    private final PrintStream out;\n\n    private final CommandLine commandLine;\n\n    private final CommandDefine command;\n\n    public CommandInterpreterImpl(PrintStream out, CommandLine commandLine, CommandDefine command) {\n        super();\n        this.out = out;\n        this.commandLine = commandLine;\n        this.command = command;\n    }\n\n    public void print(String msg) {\n        out.print(msg);\n    }\n\n    public void println(String msg) {\n        out.println(msg);\n    }\n\n    public void println(String msg, Object... args) {\n        msg = msg.replaceAll(\"\\\\{\\\\}\", \"%s\");\n        String str = String.format(msg, args);\n        println(str);\n    }\n\n    public void println() {\n        out.println();\n    }\n\n    public boolean hasOption(String key) {\n        return commandLine.hasOption(key);\n    }\n\n    public int getOption(String key, int defaultValue) throws BusinessException {\n\n        CommandOptionDefine option = command.getOption(key);\n        String name = option.getName();\n\n        String value = commandLine.getOptionValue(key);\n        if (StringUtils.isEmpty(value)) {\n            return defaultValue;\n        }\n        value = value.trim();\n\n        try {\n            return Integer.parseInt(value.trim());\n        } catch (NumberFormatException e) {\n            throw new CommandException(\n                    CommandErrorCode.ARG_PARSE_TO_INT_ERROR.getName(), e)\n                    .put(key, key).put(name, name).put(\"value\", value)\n                    .setMessage(\"解析选项值出错，选项 -${key} 或 --${name} 的值不是数字类型： {value}\");\n        }\n    }\n\n    public String getOption(String key) {\n        return commandLine.getOptionValue(key);\n    }\n\n    public String getOption(String key, String defaultValue) {\n        String value = getOption(key);\n        if (value == null) {\n            return defaultValue;\n        }\n        return value;\n    }\n\n    public Properties getOption(String key, Properties defaultValue) {\n        Properties value = commandLine.getOptionProperties(key);\n        if (value == null) {\n            return defaultValue;\n        }\n        return value;\n    }\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/impl/CommandLineApplicationListener.java",
    "content": "package com.terran4j.commons.jfinger.impl;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationListener;\nimport org.springframework.context.event.ContextRefreshedEvent;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.jfinger.Command;\nimport com.terran4j.commons.jfinger.CommandDefine;\nimport com.terran4j.commons.jfinger.CommandGroup;\nimport com.terran4j.commons.jfinger.CommandGroupDefine;\nimport com.terran4j.commons.jfinger.CommandGroups;\nimport com.terran4j.commons.jfinger.CommandOption;\nimport com.terran4j.commons.jfinger.CommandOptionDefine;\n\n@Service\npublic class CommandLineApplicationListener implements ApplicationListener<ContextRefreshedEvent> {\n\t\n\tpublic static final String PROP_KEY_DISABLE = \"terran4j.jfinger.disable\";\n\t\n\tpublic static final String PROP_KEY_PROMPT = \"terran4j.jfinger.prompt\";\n\t\n\tpublic static final String PROMPT_DEFAULT = \"jfinger\";\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(CommandLineApplicationListener.class);\n\n\tprivate static String PROMPT = PROP_KEY_PROMPT;\n\t\n\tpublic static final String getPrompt() {\n\t\treturn PROMPT;\n\t}\n\t\n\tprivate CommandLineService service = null;\n\t\n\tprivate Thread thread = null;\n\n\tpublic CommandLineApplicationListener() {\n\t\tsuper();\n\t}\n\n\t@Override\n\tpublic void onApplicationEvent(ContextRefreshedEvent event) {\n\n\t\tApplicationContext app = event.getApplicationContext();\n\t\t\n\t\t// 从配置上判断用户是否禁用了命令行服务。\n\t\tString cliDisable = app.getEnvironment().getProperty(PROP_KEY_DISABLE);\n\t\tif (cliDisable != null && cliDisable.trim().equalsIgnoreCase(\"true\")) {\n\t\t\tif (log.isWarnEnabled()) {\n\t\t\t\tlog.warn(\"Command Line Service was disable, will not be started.\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// 如果没有任何命令被注册，也不需要启用命令行服务。\n\t\tMap<String, Object> beans = app.getBeansWithAnnotation(CommandGroup.class);\n\t\tif (beans == null || beans.size() == 0) {\n\t\t\tif (log.isWarnEnabled()) {\n\t\t\t\tlog.warn(\"No Bean has Annotation: @{}. Command Line Service will not be started.\",//\n\t\t\t\t\t\tCommandGroup.class.getSimpleName());\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// 获取提示符。\n\t\tString cliPrompt = app.getEnvironment().getProperty(PROP_KEY_PROMPT);\n\t\tif (!StringUtils.isEmpty(cliPrompt)) {\n\t\t\tcliPrompt = cliPrompt.trim();\n\t\t} else {\n\t\t\tcliPrompt = PROMPT_DEFAULT;\n\t\t}\n\t\tPROMPT = cliPrompt;\n\n\t\t// 解析所有的命令行。\n\t\tCommandGroups groups = new CommandGroups();\n\t\tIterator<String> it = beans.keySet().iterator();\n\t\twhile (it.hasNext()) {\n\t\t\tString beanName = it.next();\n\t\t\tObject bean = app.getBean(beanName);\n\t\t\tClass<?> beanClass = bean.getClass();\n\t\t\tList<Method> commandMethods = getCommandMethod(beanClass);\n\t\t\tif (commandMethods.size() == 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t\n\t\t\t// 解析命令组。\n\t\t\tCommandGroup commandGroup = beanClass.getAnnotation(CommandGroup.class);\n\t\t\tCommandGroupDefine commandGroupDefine = new CommandGroupDefine(commandGroup);\n\t\t\tif (StringUtils.isEmpty(commandGroup.name())) {\n\t\t\t\tString groupName = beanName;\n\t\t\t\tif (groupName.endsWith(\"Command\") && groupName.length() > \"Command\".length()) {\n\t\t\t\t\tgroupName = groupName.substring(0, groupName.length() - \"Command\".length());\n\t\t\t\t}\n\t\t\t\tcommandGroupDefine.setName(groupName);\n\t\t\t}\n\t\t\t\n\t\t\t// 解析通用命令选项。\n\t\t\tList<CommandOptionDefine> commonOptions = new ArrayList<CommandOptionDefine>();\n\t\t\tCommandOption[] commandGroupOptions = commandGroup.options();\n\t\t\tif (commandGroupOptions != null && commandGroupOptions.length > 0) {\n\t\t\t\tfor (CommandOption commandOption : commandGroupOptions) {\n\t\t\t\t\tCommandOptionDefine commonOption = new CommandOptionDefine(commandOption);\n\t\t\t\t\tcommonOptions.add(commonOption);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// 解析本组命令。\n\t\t\tfor (Method commandMethod : commandMethods) {\n\t\t\t\tCommandDefine commandDefine = new CommandDefine(commandGroupDefine, bean, commandMethod, beanName);\n\t\t\t\tcommandDefine.addOptions(commonOptions);\n\t\t\t\tcommandGroupDefine.addCommand(commandDefine);\n\t\t\t}\n\t\t\t\n\t\t\t// 记录下本命令组。\n\t\t\tgroups.addCommandGroup(commandGroupDefine);\n\t\t}\n\t\t\n\t\tservice = new CommandLineService();\n\t\tservice.setCommands(groups);\n\t\tservice.setPrompt(cliPrompt);\n\t\t\n\t\tthread = new CommandLineTask(service);\n\t\tthread.setDaemon(false); // 线程一直存在，不让主进程退出。\n\t\tthread.setName(\"JFinger Demo Thread\");\n\t\tthread.start();\n\t\t\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"Command Line Service is started.\");\n\t\t}\n\t}\n\t\n\tprivate List<Method> getCommandMethod(Class<?> beanClass) {\n\t\tList<Method> commandMethods = new ArrayList<Method>();\n\t\t\n\t\tMethod[] methods = beanClass.getMethods();\n\t\tif (methods == null || methods.length == 0) {\n\t\t\treturn commandMethods;\n\t\t}\n\t\t\n\t\tfor (int i = 0; i < methods.length; i++) {\n\t\t\tMethod method = methods[i];\n\t\t\tCommand command = method.getAnnotation(Command.class);\n\t\t\tif (command == null) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tcommandMethods.add(method);\n\t\t}\n\t\t\n\t\treturn commandMethods;\n\t}\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/impl/CommandLineService.java",
    "content": "package com.terran4j.commons.jfinger.impl;\n\nimport java.io.PrintStream;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.List;\n\nimport org.apache.commons.cli.CommandLine;\nimport org.apache.commons.cli.CommandLineParser;\nimport org.apache.commons.cli.DefaultParser;\nimport org.apache.commons.cli.Option;\nimport org.apache.commons.cli.Option.Builder;\nimport org.apache.commons.cli.Options;\nimport org.apache.commons.cli.ParseException;\nimport org.apache.commons.cli.UnrecognizedOptionException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.jfinger.CommandDefine;\nimport com.terran4j.commons.jfinger.CommandErrorCode;\nimport com.terran4j.commons.jfinger.CommandException;\nimport com.terran4j.commons.jfinger.CommandExecutor;\nimport com.terran4j.commons.jfinger.CommandGroupDefine;\nimport com.terran4j.commons.jfinger.CommandGroups;\nimport com.terran4j.commons.jfinger.CommandInterpreter;\nimport com.terran4j.commons.jfinger.CommandOptionDefine;\nimport com.terran4j.commons.jfinger.OptionType;\nimport com.terran4j.commons.util.error.BusinessException;\n\n@Service\npublic class CommandLineService {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(CommandLineService.class);\n\n\tprivate static final Util util = new Util();\n\n\tprivate static final CommandLineParser parser = new DefaultParser();\n\t\n\tprivate String prompt = CommandLineApplicationListener.PROMPT_DEFAULT;\n\t\n\tprivate CommandGroups commands = new CommandGroups();\n\n\tpublic CommandLineService() {\n\t\tsuper();\n\t}\n\n\tpublic CommandGroups getCommands() {\n\t\treturn commands;\n\t}\n\n\tpublic void setCommands(CommandGroups commands) {\n\t\tthis.commands = commands;\n\t}\n\n\tpublic String getPrompt() {\n\t\treturn prompt;\n\t}\n\n\tpublic void setPrompt(String prompt) {\n\t\tthis.prompt = prompt;\n\t}\n\n\tpublic final boolean execute(String command, PrintStream out) {\n\t\tif (StringUtils.isEmpty(command)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"command: {}\", command);\n\t\t}\n\n\t\tcommand = command.trim();\n\t\tif (\"quit\".equals(command)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Print All Commands Help\n\t\tif (command.startsWith(\"help \") || command.equals(\"help\")) {\n\t\t\tString[] helpArgs = util.split(command, \" \");\n\t\t\tif (helpArgs.length > 2) {\n\t\t\t\tout.println(getHelp(helpArgs[1], helpArgs[2]));\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (helpArgs.length > 1) {\n\t\t\t\tout.println(getHelp(helpArgs[1]));\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (helpArgs.length > 0) {\n\t\t\t\tout.println(getHelp());\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tString[] args = parseCommand(command);\n\t\t\tif (args.length < 2) {\n\t\t\t\tout.println(getHelpPrompt());\n\t\t\t\tout.println(\"命令格式不正确，至少要输入：  [命令组名称] [命令名称] ，请用上面的 help 命令查询其详细用法\");\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tString group = args[0];\n\t\t\tCommandGroupDefine commandGroup = commands.get(group);\n\t\t\tif (commandGroup == null) {\n\t\t\t\tout.println(getHelpPrompt());\n\t\t\t\tout.println(\"不存在此命令组： \" + group + \"，请用上面的 help 命令查询其详细用法。\\n\");\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (commandGroup.size() == 0) {\n\t\t\t\tout.println(\"命令组【 \" + group + \"】中没有任何命令！\\n\");\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tString commandName = args[1];\n\t\t\tCommandDefine commandConfig = commandGroup.getCommand(commandName);\n\t\t\tif (commandConfig == null) {\n\t\t\t\tout.println(\"在命令组【\" + group + \"】中，不存在此命令： \" + commandName);\n\t\t\t\tout.println(getHelp(group));\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tString[] commandArgs = Arrays.copyOfRange(args, 2, args.length);\n\t\t\texecute(group, commandName, commandConfig, commandArgs, out);\n\n\t\t} catch (CommandException ce) {\n\t\t\tString msg = ce.getMessage();\n\t\t\tout.println(msg);\n\t\t} catch (Throwable e) {\n\t\t\tprintHelpPrompt(e, out);\n\t\t}\n\t\treturn true;\n\t}\n\n\tprivate void execute(String groupName, String commandName, CommandDefine command, String[] args, PrintStream out)\n\t\t\tthrows BusinessException {\n\n\t\tOptions options = new Options();\n\t\tList<CommandOptionDefine> optionConfigs = command.getOptions();\n\t\tif (optionConfigs != null && optionConfigs.size() > 0) {\n\t\t\tfor (CommandOptionDefine optionConfig : optionConfigs) {\n\t\t\t\tString key = optionConfig.getKey();\n\t\t\t\tString name = optionConfig.getName();\n\t\t\t\tString desc = optionConfig.getDesc();\n\n\t\t\t\tOptionType type = optionConfig.getType();\n\t\t\t\tif (type == OptionType.BOOLEAN) {\n\t\t\t\t\toptions.addOption(key, name, false, desc);\n\t\t\t\t} else if (type == OptionType.PROPERTIES) {\n\t\t\t\t\tOption option = Option.builder(key).argName(\"property=value\") //\n\t\t\t\t\t\t\t.numberOfArgs(5).valueSeparator('=').desc(desc) //\n\t\t\t\t\t\t\t.build();\n\t\t\t\t\toptions.addOption(option);\n\t\t\t\t} else {\n\t\t\t\t\tBuilder builder = Option.builder(key).hasArg();\n\t\t\t\t\tif (!StringUtils.isEmpty(name)) {\n\t\t\t\t\t\tbuilder = builder.longOpt(name);\n\t\t\t\t\t}\n\t\t\t\t\tOption option = builder.desc(desc).build();\n\t\t\t\t\toptions.addOption(option);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tCommandLine commandLine = null;\n\t\ttry {\n\t\t\tcommandLine = parser.parse(options, args);\n\t\t} catch (UnrecognizedOptionException e) {\n\t\t\tString optionKey = e.getOption();\n\t\t\tout.println(\"无效的命令行选项： \" + optionKey);\n\t\t\tout.println(getHelp(groupName, commandName));\n\t\t\treturn;\n\t\t} catch (ParseException e) {\n\t\t\tout.println(\"解析命令出错：\" + e.getMessage());\n\t\t\tout.println(getHelp(groupName, commandName));\n\t\t\treturn;\n\t\t}\n\n\t\tCommandExecutor executor = command.getExecutor();\n\t\tCommandInterpreter ci = new CommandInterpreterImpl(out, commandLine, command);\n\n\t\tif (optionConfigs != null && optionConfigs.size() > 0) {\n\t\t\tfor (CommandOptionDefine optionConfig : optionConfigs) {\n\t\t\t\tif (optionConfig.isRequired() &&\n\t\t\t\t\t\toptionConfig.getType() != OptionType.BOOLEAN) {\n\t\t\t\t\tString key = optionConfig.getKey();\n\t\t\t\t\tString name = optionConfig.getName();\n\t\t\t\t\tString value = ci.getOption(key);\n\t\t\t\t\tif (value == null) {\n\t\t\t\t\t\tthrow new CommandException(CommandErrorCode.OPTION_KEY_EMPTY.getName()) //\n\t\t\t\t\t\t\t\t.put(\"group\", groupName) // \n\t\t\t\t\t\t\t\t.put(\"commandName\", commandName) // \n\t\t\t\t\t\t\t\t.put(\"optionKey\", key) // \n\t\t\t\t\t\t\t\t.put(\"optionName\", name) // \n\t\t\t\t\t\t\t\t.setMessage(\"命令 [${group} ${commandName}] 需要 ${optionName} 选项，\"\n\t\t\t\t\t\t\t\t\t\t+ \"请在命令后面附上 -${optionKey} [${optionName}]\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\texecutor.execute(ci);\n\t}\n\n\tprivate void printHelpPrompt(Throwable e, PrintStream out) {\n\t\te.printStackTrace(out);\n\t\tout.println(\"执行命令出错: \" + e.getMessage());\n\t\tout.println(getHelpPrompt());\n\t}\n\n\t/**\n\t * @return\n\t */\n\tpublic String getHelpPrompt() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(\"请用 help 查询命令的详细用法，如：\\n\");\n\t\tsb.append(\"输入：\\n\");\n\t\tsb.append(\"    help\\n\");\n\t\tsb.append(\"查看所有的命令组。\\n\\n\");\n\n\t\tsb.append(\"输入：\\n\");\n\t\tsb.append(\"    help [groupName]\\n\");\n\t\tsb.append(\"查看指定命令组的详细信息，如：\\n\");\n\t\tsb.append(\"    help system\\n\\n\");\n\n\t\tsb.append(\"输入：\\n\");\n\t\tsb.append(\"    help [groupName] [commandName]\\n\");\n\t\tsb.append(\"查看指定命令的详细信息，如：\\n\");\n\t\tsb.append(\"    help system prop\\n\\n\");\n\n\t\treturn sb.toString();\n\t}\n\n\tpublic String getHelp() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(\"欢迎使用由 terran4j 提供的命令行服务。\\n\\n\");\n\t\tsb.append(\"当前程序有以下命令可供使用：\\n\");\n\t\tIterator<String> it = commands.keySet().iterator();\n\t\twhile (it.hasNext()) {\n\t\t\tString groupName = it.next();\n\t\t\tif (StringUtils.isEmpty(groupName)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tCommandGroupDefine group = commands.get(groupName);\n\t\t\tif (group == null || group.size() == 0) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tsb.append(\"命令：  \").append(groupName).append(\" [\");\n\t\t\tList<CommandDefine> commands = group.getCommands();\n\t\t\tboolean notFirst = false;\n\t\t\tfor (int i = 0; i < commands.size(); i++) {\n\t\t\t\tCommandDefine command = commands.get(i);\n\t\t\t\tString name = command.getName();\n\t\t\t\tif (notFirst) {\n\t\t\t\t\tsb.append(\" | \");\n\t\t\t\t} else {\n\t\t\t\t\tnotFirst = true;\n\t\t\t\t}\n\t\t\t\tsb.append(name);\n\t\t\t}\n\t\t\tsb.append(\"]\\n\");\n\t\t\tsb.append(\"说明：  \").append(group.getDesc()).append(\"\\n\\n\");\n\t\t}\n\n\t\tsb.append(\"\\n\").append(getHelpPrompt());\n\n\t\treturn sb.toString();\n\t}\n\n\tpublic String getHelp(String groupName) {\n\t\tif (StringUtils.isEmpty(groupName)) {\n\t\t\treturn getHelp();\n\t\t}\n\t\tgroupName = groupName.trim();\n\n\t\tCommandGroupDefine group = commands.get(groupName);\n\t\tif (group == null) {\n\t\t\treturn groupName + \" not found, all command group list:\\n\" + getHelp();\n\t\t}\n\n\t\tStringBuilder sb = new StringBuilder();\n\t\tsb.append(\"命令组：  \").append(groupName).append(\"\\n\");\n\t\tsb.append(\"说明：  \").append(group.getDesc()).append(\"\\n\");\n\n\t\tsb.append(\"命令列表：\\n\");\n\t\tList<CommandDefine> commandList = group.getCommands();\n\t\tfor (int i = 0; i < commandList.size(); i++) {\n\t\t\tCommandDefine command = commandList.get(i);\n\t\t\tString name = command.getName();\n\t\t\tif (StringUtils.isEmpty(name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 描述中有多行时，每行前加 8 个空格。\n\t\t\tString desc = command.getDesc();\n\t\t\tdesc = desc.replaceAll(\"\\n\", \"\\n        \");\n\n\t\t\tsb.append(\"    \").append(name).append(\": \").append(desc).append(\"\\n\");\n\t\t}\n\n\t\treturn sb.toString();\n\t}\n\n\tpublic String getHelp(String groupName, String name) {\n\t\tif (StringUtils.isEmpty(groupName)) {\n\t\t\treturn getHelp();\n\t\t}\n\t\tgroupName = groupName.trim();\n\n\t\tCommandGroupDefine group = commands.get(groupName);\n\t\tif (group == null) {\n\t\t\treturn groupName + \" not found, all command group list:\\n\" + getHelp();\n\t\t}\n\n\t\tif (StringUtils.isEmpty(name)) {\n\t\t\treturn getHelp(groupName);\n\t\t}\n\t\tname = name.trim();\n\n\t\tCommandDefine command = group.getCommand(name);\n\t\tif (command == null) {\n\t\t\treturn name + \" not found in group, the commands list in the group:\\n\" + getHelp(groupName);\n\t\t}\n\t\treturn command.getHelp();\n\t}\n\n\tpublic void printPrompt(PrintStream out) {\n\t\tout.print(\"\\n\" + getPrompt() + \">\");\n\t}\n\n\tpublic static final int[] nextIndex(String command, int fromIndex) throws BusinessException {\n\n\t\tint currentIndex = fromIndex;\n\t\tint lastIndex = command.length() - 1;\n\t\tfor (; currentIndex <= lastIndex; currentIndex++) {\n\t\t\tchar currentChar = command.charAt(currentIndex);\n\t\t\tif (currentChar != ' ') {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (currentIndex >= lastIndex) {\n\t\t\treturn null;\n\t\t}\n\t\tint startIndex = currentIndex;\n\n\t\tint endIndex = -1;\n\t\tchar firstChar = command.charAt(startIndex);\n\t\tif (firstChar == '\"') { // 以\"\"包裹的参数。\n\t\t\tcurrentIndex = startIndex + 1;\n\t\t\tint nextIndex = command.indexOf(\"\\\"\", currentIndex);\n\t\t\twhile (nextIndex > currentIndex && nextIndex <= lastIndex) {\n\t\t\t\tchar prevChar = command.charAt(nextIndex - 1);\n\t\t\t\tif (prevChar != '\\\\') {\n\t\t\t\t\tendIndex = nextIndex;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcurrentIndex = nextIndex + 1;\n\t\t\t\tnextIndex = command.indexOf(\"\\\"\", currentIndex);\n\t\t\t}\n\t\t\tif (endIndex == -1) {\n\t\t\t\tString errorPart = command.substring(startIndex);\n\t\t\t\tif (errorPart.length() > 10) {\n\t\t\t\t\terrorPart = errorPart.substring(0, 10);\n\t\t\t\t}\n\t\t\t\tthrow new CommandException(CommandErrorCode.ARG_QUOTE_NOT_CLOSE.getName())\n\t\t\t\t\t\t.put(\"startIndex\", startIndex)\n\t\t\t\t\t\t.put(\"errorPart\", errorPart)\n\t\t\t\t\t\t.put(\"command\", command)\n\t\t\t\t\t\t.setMessage(\"命令中第${startIndex}个字符\\\"号没有另一个\\\"作为结束符: ${errorPart}\");\n\t\t\t}\n\n\t\t} else { // 没有以\"\"包裹的参数。\n\t\t\tendIndex = command.indexOf(\" \", startIndex);\n\t\t}\n\t\tif (endIndex <= startIndex) {\n\t\t\treturn null;\n\t\t}\n\t\treturn new int[] { startIndex, endIndex };\n\t}\n\n\tpublic static final String[] parseCommand(String command) throws BusinessException {\n\t\tList<String> args = new ArrayList<String>();\n\t\tint fromIndex = 0;\n\t\tint lastIndex = command.length() - 1;\n\t\twhile (fromIndex <= lastIndex) {\n\t\t\tString arg = null;\n\t\t\tint[] pos = nextIndex(command, fromIndex);\n\t\t\tif (pos == null) {\n\t\t\t\targ = command.substring(fromIndex);\n\t\t\t\tfromIndex = lastIndex + 1;\n\t\t\t} else {\n\t\t\t\targ = command.substring(pos[0], pos[1] + 1);\n\t\t\t\tfromIndex = pos[1] + 1;\n\t\t\t}\n\t\t\tif (StringUtils.isEmpty(arg)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\targ = arg.trim();\n\t\t\tif (arg.startsWith(\"\\\"\") && arg.endsWith(\"\\\"\")) {\n\t\t\t\tif (arg.length() <= 2) {\n\t\t\t\t\targ = \"\";\n\t\t\t\t} else {\n\t\t\t\t\targ = arg.substring(1, arg.length() - 1);\n\t\t\t\t}\n\t\t\t}\n\t\t\targs.add(arg);\n\t\t}\n\t\treturn args.toArray(new String[args.size()]);\n\t}\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/impl/CommandLineTask.java",
    "content": "package com.terran4j.commons.jfinger.impl;\n\nimport java.io.PrintStream;\nimport java.util.NoSuchElementException;\nimport java.util.Scanner;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\n\npublic class CommandLineTask extends Thread {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(CommandLineTask.class);\n\n\tprivate final CommandLineService service;\n\n\tpublic CommandLineTask(CommandLineService service) {\n\t\tsuper();\n\t\tthis.service = service;\n\t}\n\n\tpublic void run() {\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"start Command Line Task.\");\n\t\t}\n\t\tScanner sc = new Scanner(System.in);\n\t\tPrintStream out = System.out;\n\t\ttry {\n\t\t\tboolean running = true;\n\t\t\t\n\t\t\t// 等待程序彻底启动了，再显示提示符。\n\t\t\ttry {\n\t\t\t\tThread.sleep(200);\n\t\t\t} catch (InterruptedException e) {\n\t\t\t\t// ignore.\n\t\t\t}\n\t\t\tout.println();\n\t\t\tout.println(\"JFinger Command Line Service is starting...\");\n\t\t\tservice.printPrompt(out);\n\n\t\t\twhile (running) {\n\t\t\t\tString commandLine = null;\n\t\t\t\ttry {\n\t\t\t\t\tcommandLine = sc.nextLine();\n\t\t\t\t} catch (NoSuchElementException e) {\n\t\t\t\t}\n\t\t\t\tif (StringUtils.hasText(commandLine)) {\n\t\t\t\t\trunning = service.execute(commandLine, out);\n\t\t\t\t}\n\t\t\t\tservice.printPrompt(out);\n\t\t\t}\n\t\t} finally {\n\t\t\tif (sc != null) {\n\t\t\t\tsc.close();\n\t\t\t}\n\t\t\tout.println(\"Command Line Console Closed.\");\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"stoped Command Line Task.\");\n\t\t\t}\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/impl/DynMethodCommandExecutor.java",
    "content": "package com.terran4j.commons.jfinger.impl;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\nimport com.terran4j.commons.jfinger.CommandExecutor;\nimport com.terran4j.commons.jfinger.CommandInterpreter;\nimport com.terran4j.commons.util.Strings;\n\npublic class DynMethodCommandExecutor implements CommandExecutor {\n\n\tprivate final Object bean;\n\n\tprivate final Method method;\n\n\tprivate final String beanName;\n\n\tpublic DynMethodCommandExecutor(Object bean, Method method, String beanName) {\n\t\tsuper();\n\t\tthis.bean = bean;\n\t\tthis.method = method;\n\t\tthis.beanName = beanName;\n\t}\n\n\t@Override\n\tpublic void execute(CommandInterpreter ci) {\n\t\tObject[] args = { ci };\n\t\ttry {\n\t\t\tmethod.invoke(bean, args);\n\t\t} catch (IllegalAccessException e) {\n\t\t\tString msg = String.format(\"无法访问Spring Bean【%s】的方法【%s】!\", beanName, method.getName());\n\t\t\tci.println(msg);\n\t\t} catch (IllegalArgumentException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t} catch (InvocationTargetException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t} catch (Exception e) {\n\t\t\tString errorMessage = Strings.getString(e);\n\t\t\tci.println(errorMessage);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/com/terran4j/commons/jfinger/impl/Util.java",
    "content": "package com.terran4j.commons.jfinger.impl;\n\nimport java.io.BufferedReader;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.io.PrintWriter;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport com.terran4j.commons.jfinger.Encoding;\n\npublic class Util {\n\t\n\tprivate final static int DEFAULT_BUFFERSIZE = 1024 * 4;\n\n\tprivate final static int DEFAULT_SLEEP_COUNT = 3;\n\n\tpublic String getString(InputStream in) {\n\t\treturn this.getString(in, Encoding.getDefaultEncoding());\n\t}\n\n\tpublic String getString(Throwable t) {\n\t\tif (t == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tByteArrayOutputStream out = new ByteArrayOutputStream();\n\t\ttry {\n\t\t\tPrintWriter pw = new PrintWriter(out);\n\t\t\tt.printStackTrace(pw);\n\t\t\tpw.flush();\n\t\t\tString str = new String(out.toByteArray(), Encoding.UTF8.getName());\n\t\t\treturn str;\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\te.printStackTrace();\n\t\t\tthrow new RuntimeException(e);\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tout.close();\n\t\t\t} catch (Exception e) {\n\t\t\t\t// ignore the error.\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic String getString(Class<?> clazz, String fileName) {\n\t\tString path = null;\n\t\tClassLoader loader = null;\n\t\tif (clazz == null) {\n\t\t\tpath = fileName;\n\t\t\tloader = getClass().getClassLoader();\n\t\t} else {\n\t\t\tpath = getClassPath(clazz, fileName);\n\t\t\tloader = clazz.getClassLoader();\n\t\t}\n\t\t\n\t\tInputStream in = loader.getResourceAsStream(path);\n\t\tif (in == null) {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tString str = getString(in);\n\t\t\tif (str == null) {\n\t\t\t\tstr = \"\";\n\t\t\t}\n\t\t\treturn str;\n\t\t} finally {\n\t\t\tif (in != null) {\n\t\t\t\ttry {\n\t\t\t\t\tin.close();\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic InputStream toInputStream(String s) {\n\t\tif (s == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tInputStream in = new ByteArrayInputStream(s.getBytes(Encoding.UTF8.getName()));\n\t\t\treturn in;\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\te.printStackTrace();\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tpublic String getString(InputStream in, Encoding encoding) {\n\t\t/*\n\t\t * To convert the InputStream to String we use the\n\t\t * BufferedReader.readLine() method. We iterate until the BufferedReader\n\t\t * return null which means there's no more data to read. Each line will\n\t\t * appended to a StringBuilder and returned as String.\n\t\t */\n\t\tStringBuilder sb = new StringBuilder();\n\t\ttry {\n\t\t\tif (encoding == null) {\n\t\t\t\tencoding = Encoding.getDefaultEncoding();\n\t\t\t}\n\t\t\tInputStreamReader inr = new InputStreamReader(in, encoding.getName());\n\t\t\tBufferedReader reader = new BufferedReader(inr);\n\n\t\t\tString line = null;\n\t\t\twhile ((line = reader.readLine()) != null) {\n\t\t\t\tsb.append(line + \"\\n\");\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\te.printStackTrace();\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t\t// #1005 去掉in.close()语句。\n\n\t\treturn sb.toString();\n\n\t}\n\t\n\tpublic long copy(InputStream input, OutputStream output) throws IOException {\n        // 接口空校验，解决抛出异常，引发异常处理逻辑再次抛出异常而告警的问题\n        long count = 0;\n        if (input == null || output == null) {\n            return count;\n        }\n        byte[] buffer = new byte[DEFAULT_BUFFERSIZE];\n        int n = 0;\n        while (true) {\n            int read = input.read(buffer);\n            if (read < 0) {\n                break;\n            }\n            output.write(buffer, 0, read);\n            count += read;\n            output.flush();\n            n++;\n            if (n % DEFAULT_SLEEP_COUNT == 0) {\n                try {\n                    Thread.sleep(0);\n                } catch (InterruptedException e) {\n                    // ignore\n                }\n            }\n        }\n        return count;\n    }\n\n\tpublic String[] split(String str) {\n\t\treturn split(str, \",\", -1);\n\t}\n\n\tpublic String[] split(String str, String split) {\n\t\treturn split(str, split, -1);\n\t}\n\n\tpublic String[] split(String str, String split, int limit) {\n\t\tif (str == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tstr = str.trim();\n\n\t\tif (str.length() == 0) {\n\t\t\treturn new String[0];\n\t\t}\n\n\t\tif (split == null || split.length() == 0) {\n\t\t\treturn new String[] { str };\n\t\t}\n\n\t\tList<String> items = new ArrayList<String>();\n\t\tString[] strs = null;\n\t\tif (limit > 0) {\n\t\t\tstrs = str.split(split, limit);\n\t\t} else {\n\t\t\tstrs = str.split(split);\n\t\t}\n\n\t\tif (strs != null && strs.length > 0) {\n\t\t\tfor (String s : strs) {\n\t\t\t\tif (s == null || s.trim().length() == 0) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\titems.add(s.trim());\n\t\t\t}\n\t\t}\n\n\t\tString[] ss = new String[items.size()];\n\t\treturn items.toArray(ss);\n\t}\n\n\tpublic String[] split(String str, String begin, String end) {\n\t\tif (str == null || str.trim().length() == 0 || begin == null || begin.trim().length() == 0 || end == null\n\t\t\t\t|| end.trim().length() == 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\tList<String> list = new ArrayList<String>();\n\t\tint from = 0;\n\t\twhile (true) {\n\t\t\tint m = str.indexOf(begin, from);\n\t\t\tint n = str.indexOf(end, from);\n\n\t\t\tif (m >= 0 && m < str.length() && n > m && n < str.length()) {\n\t\t\t\tString s = str.substring(m, n + end.length());\n\t\t\t\tlist.add(s);\n\t\t\t\tfrom = n + end.length();\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tString[] rets = new String[list.size()];\n\t\treturn list.toArray(rets);\n\t}\n\n\t/**\n\t * 根据类对象， 获取同包下的文件的资源路径。\n\t * \n\t * @param clazz\n\t * @param fileName\n\t * @return\n\t */\n\tpublic String getClassPath(final Class<?> clazz, String fileName) {\n\t\tPackage classPackage = clazz.getPackage();\n\t\tif (classPackage != null) {\n\t\t\treturn classPackage.getName().replace('.', '/') + \"/\" + fileName;\n\t\t} else {\n\t\t\treturn fileName;\n\t\t}\n\n\t}\n\n\t/**\n\t * \n\t */\n\tpublic String encode(String str) {\n\t\ttry {\n\t\t\treturn URLEncoder.encode(str, Encoding.UTF8.getName());\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\te.printStackTrace();\n\t\t\tthrow new RuntimeException(\"Unsupported UTF-8 Encoding\");\n\t\t}\n\t}\n\n\tpublic String decode(String str) {\n\t\ttry {\n\t\t\treturn URLDecoder.decode(str, Encoding.UTF8.getName());\n\t\t} catch (UnsupportedEncodingException e) {\n\t\t\te.printStackTrace();\n\t\t\tthrow new RuntimeException(\"Unsupported UTF-8 Encoding\");\n\t\t}\n\t}\n\t\n\t\n}\n"
  },
  {
    "path": "commons-jfinger/src/main/java/error.properties",
    "content": "\nk1 = 111\nk3 = 333"
  },
  {
    "path": "commons-jfinger/src/test/java/cmd.txt",
    "content": "\n## 缺少命令名。\nlog\n\n## 错误的命令组名。\nlog2 setLevel\n\n\n## 错误的命令名。\nlog setLevel2\n\n## 缺少必需选项。\nlog setLevel\n\n## 缺少必需选项。\nlog setLevel -n \"com.terran4j\"\n\n## 错误的选项键。\nlog setLevel -m \"com.terran4j\"\n\n## 错误的选项名。\nlog setLevel --logName \"com.terran4j\"\n\n## 正确的命令（用键的方式）\nlog setLevel -n \"com.terran4j\" -l warn \n\n## 正确的命令（用名称的方式）\nlog setLevel --loggerName \"com.terran4j\" -logLevel warn \n\n"
  },
  {
    "path": "commons-jfinger/src/test/java/com/terran4j/test/commons/jfinger/JFingerTestApplication.java",
    "content": "package com.terran4j.test.commons.jfinger;\n\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.util.Enumeration;\nimport java.util.Properties;\nimport java.util.ResourceBundle;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.jfinger.EnableJFinger;\n\n@EnableJFinger\n@SpringBootApplication\npublic class JFingerTestApplication {\n    \n    public static void main(String[] args) throws Exception {\n        SpringApplication.run(JFingerTestApplication.class, args);\n        \n        String fileName = \"error.properties\";\n        \n        Properties props = new Properties();\n        Enumeration<URL> urls = SpringApplication.class.getClassLoader() //\n        \t\t.getResources(fileName);\n        if (urls != null && urls.hasMoreElements()) {\n        \twhile (urls.hasMoreElements()) {\n        \t\tURL url = urls.nextElement();\n        \t\tInputStream in = url.openStream();\n        \t\tif (in != null) {\n        \t\t\ttry {\n        \t\t\t\tprops.load(in);\n        \t\t\t} catch (Exception e) {\n        \t\t\t\ttry {\n        \t\t\t\t\tin.close();\n        \t\t\t\t} catch (Exception e1) {\n        \t\t\t\t\t// ignore.\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n        \t\t}\n        \t}\n        }\n        \n//        ResourceBundle\n        \n        System.out.println(props);\n        System.out.println(\"k1 = \" + props.getProperty(\"k1\"));\n        System.out.println(\"k2 = \" + props.getProperty(\"k2\"));\n        System.out.println(\"k3 = \" + props.getProperty(\"k3\"));\n    }\n\n}\n"
  },
  {
    "path": "commons-reflux/.gitignore",
    "content": "/target/\n/.settings/\n/.classpath\n/.project\nterran4j-commons-reflux.iml\n"
  },
  {
    "path": "commons-reflux/README.md",
    "content": "\nReflux 是基于 Spring Boot + Web Socket 技术编写的一套实现“服务端推送”\n的技术框架，可以让 Java 开发人员非常简单的实现逆向通信（即客户端请求\n建立连接后，服务端主动向客户端推送消息）。\n\n## 目录\n\n* 项目背景\n* Reflux 简介\n* 同类产品\n* 源码下载\n* 软件版本说明\n* 适用读者\n* clientId 详述\n* demo 程序构成\n* 配置 terran4j 的 Maven 仓库\n* 创建 demo-reflux 项目\n* 创建 demo-reflux-server 项目\n* Reflux 服务端 —— 建立 WebSocket 端点\n* 创建 demo-reflux-client 项目\n* Reflux 客户端 —— 建立 WebSocket 连接\n* 服务端推送消息到客户端\n* 分布式解决方案\n\n\n## 项目背景\n传统的 HTTP 通信是单向的，即由客户端发送请求，服务端只能被动响应，\n不能主动向客户端推送消息。\n然而有很多业务场景是需要服务端主动向客户端推送消息的，为了能实现这个目的，\n传统技术有两种：\n\n1. 让客户端起一个 demo 线程，定期轮询请求服务端，服务端有消息就返回消息。\n2. 让客户端发起一个长连接的 HTTP 请求，服务端对这个请求“hold”住不放，\n    直到有消息时才返回消息。\n3. 完全基于 TCP 层自定义协议实现。\n\n前两种实现方式都只能算“伪推送”，网络消耗较大，运行效率较低。\n第3种方式，可能要占用额外的端口（而不是直复用 HTTP 端口），并且开发较复杂，\n部署也复杂。\n\n在以上背景下，Web Socket 技术就应运而生了，它基于 TCP 底层实现真正的双向通信，\n运行效率比“伪推送”高得多，又不占用额外的端口，可以说是完美解决了这个问题。\n\nSpring Boot 也集成了 Web Socket 技术，但开发方面提供的支持较少，\n要在实际场景下实现“服务端推送”仍需要编写很多代码。\n本模块就是要基于 Spring Boot + Web Socket 技术，实现逆向通信（即客户端\n请求建立连接后，能让服务端主动向客户端推送消息）。\n\n\n## Reflux 简介\n\nreflux 在英文中是“逆流、回流”的意思，考虑到web项目一般是client端主动\n发起请求，服务端只是被动响应，而 reflux 利用 WebSocket 技术实现了逆向\n推送（即服务端主动向客户端发送消息），因此用这个英文单词作为项目名称。\n\nReflux 在 Spring Boot 的基础上提供了 client 端和 server 端的 JAVA API，\n它的使用流程如下：\n\n1. 客户端向服务端发起 WebSocket 请求，请求中带一个名为 clientId 的参数，以表明此客户端的身份。\n2. 服务端在校验了 clientId 的合法性后，接受请求以建立连接。\n3. 服务端在以后（连接建立后）的任意时间点，都可以主动向客户端推送消息。\n\n交互流程如下所示：\n\n![reflux.png](http://upload-images.jianshu.io/upload_images/4489584-97386ef3b50b9d75.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n## 同类产品\n\n实际上业界也有基于以上说的3种方案做成独立产品的，如：\n\n * [goeasy](http://goeasy.io/)：  非开源、收费。\n * [socket.io](http://blog.csdn.net/jbboy/article/details/41787657)： 基于 nodejs 写的，也只有 js 的  API 。\n * [极光](http://docs.jiguang.cn/jpush/server/3rd/java_sdk/)：  非开源、收费。\n\n大部分做得好的是独立的收费产品，很少有又好用、又开源免费的产品，希望本产品可以为广大开发者们提供一种新的选择。\n\n \n## 源码下载\n\n目前笔者已将本项目作为一个开源项目来维护，源代码放在了 [这里](https://git.oschina.net/terran4j-public/commons/tree/master/commons-reflux) 。\n\n本文所用的示例代码也放在“码云”上了，欢迎大家免费下载或浏览：\n * [ demo-reflux-server ](https://git.oschina.net/terran4j-public/demo/tree/master/demo-reflux-server)\n : 服务端示例代码。\n * [ demo-reflux-client ](https://git.oschina.net/terran4j-public/demo/tree/master/demo-reflux-client)\n : 客户端示例代码。\n * [ demo-reflux ](https://git.oschina.net/terran4j-public/demo/tree/master/demo-reflux)\n : 客户端与服务端公共部分代码。\n\n\n## 软件版本说明\n\n相关软件使用的版本：\n* Java:  1.8\n* Maven:  3.3.9\n* SpringBoot:  1.5.2.RELEASE\n\n程序在以上版本均调试过，可以正常运行。\n其它版本理论上相同，但仅供参考。\n\n\n## 适用读者\n\n本文适合有Java + Maven + SpringBoot 开发经验的开发者们。\n如果您有 Java 开发经验但对Spring Boot 还不熟悉的话，建议先阅读笔者写过的一本书[ 《Spring Boot 快速入门》 ](http://www.jianshu.com/nb/14688855?order_by=seq)。\n这本书的目标是帮助有 Java 开发经验的程序员们快速掌握使用 Spring Boot 开发的基本技巧，感受到 Spring Boot 的极简开发风格及超爽编程体验。\n\n\n## clientId 详述\n\n上一节所讲到的 clientId ，是客户端的身份凭证，至于如何管理 clientId 则不在 Reflux 范围之内。\n“如何管理 clientId”，意指以下问题：\n\n * clientId 如何生成；\n * 客户端又如何获取到 clientId；\n * 建立连接时，服务端又怎么校验 clientId 的合法性；\n * 服务端如何存储 clientId 与客户端其它信息的关联。\n\n我们列举两个场景来描述 clientId 在实际项目中是怎么管理的：\n\n * 在一个移动互联网类的项目中，client 端是移动端 App 应用程序，如果用户登录功能是类似于 OAuth2.0 的方式的话，那 client 会先访问登录请求，登录成功后获得一个 access_token，这个 access_token 就可以作为我们这里的 clientId 来使用，服务端也可以通过调用账号系统API来校验 clientId 的合法性。\n * 在一个 PAAS 平台类的项目中，client 端可能是使用 PAAS 平台的应用系统。一般来说，server端（PAAS平台）会给每个应用系统分配一个appKey + appSecret 作为应用系统的凭证，client 端（应用系统的一台实例）可以用 appSecret 作为密钥，给 “appKey + 实例IP” 加密，将密文作为 clientId，server 端校验 clientId 时再用 appSecret 解密即可获知应用方的 appKey 及客户端实例的 IP。\n\n当然，这里是只是举两个场景作为例子，实际上使用“服务端推送”技术的场景是非常多的，开发者可以根据自身的业务需求进行处理。\n\n\n## demo 程序构成\n\n下面几节我们会讲解一个 demo 程序的开发，帮助我们理解 Reflux 的用法。\ndemo 程序分以下几个项目：\n\n * [ demo-reflux ](https://git.oschina.net/terran4j-public/demo/tree/master/demo-reflux)\n : 客户端与服务端公共部分代码，定义了一个名为`Hello`的Java Bean，作为消息的内容载体。\n * [ demo-reflux-server ](https://git.oschina.net/terran4j-public/demo/tree/master/demo-reflux-server)\n : 服务端代码，提供一个 WebSocket 端点供客户端连接，还提供一个Controller用于发起消息推送。\n * [ demo-reflux-client ](https://git.oschina.net/terran4j-public/demo/tree/master/demo-reflux-client)\n : 客户端代码，启动后会去连接服务端以建立 WebSocket 连接，同时会监听来自服务端的消息推送。\n\n\n## 配置 terran4j 的 Maven 仓库\n\nReflux 是笔者（terran4j）多个开源项目的其中一个项目，笔者为了方便大家使用，专门搭建了一个开放的 maven 仓库，并将所有开源项目的 jar 包发布到这个仓库中了，因此需要您在 maven 的 settings.xml 文件上配置上这个仓库，配置方法参见《[配置 terran4j 的 maven 仓库](http://www.jianshu.com/p/283cd7ce3e87)》。\n\n\n## 创建 demo-reflux 项目\n\n首先，我们基于 Spring Boot 创建名为 demo-reflux 的项目，并在 pom.xml 文件中引入 reflux 的依赖：\n\n```xml\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-reflux</artifactId>\n\t\t\t<version>1.0.2</version>\n\t\t</dependency>\n```\n\nterran4j-commons-reflux 项目的当前最新稳定本是 1.0.2 ，若有更新升级会本这里给出最新版本号。\n另外 terran4j-commons-reflux 是发布在 terran4j 的 maven 仓库中，所以 **需要在您 maven 的 settings.xml 中配置此 maven 仓库**，配置方法请参见 这篇文档 。\n\n\n整个 pom.xml 文件代码如下所示：\n\n```xml\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<groupId>terran4j</groupId>\n\t<artifactId>demo-reflux</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t<packaging>jar</packaging>\n\n\t<name>demo-reflux</name>\n\t<url>http://maven.apache.org</url>\n\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>1.5.2.RELEASE</version>\n\t</parent>\n\n\t<properties>\n\t\t<java.version>1.8</java.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-reflux</artifactId>\n\t\t\t<version>1.0.2</version>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n```\n\n然后我们定义一个名为 `Hello` 的 Java Bean，代码如下所示：\n\n```java\npackage com.terran4j.demo.reflux;\n\nimport com.terran4j.commons.util.Strings;\n\npublic class Hello {\n\n\tprivate String name;\n\t\n\tprivate String greeting;\n\t\n\tprivate long currentTime;\n\t\n\tpublic Hello() {\n\t\tsuper();\n\t}\n\t\n\tpublic Hello(String name) {\n\t\tthis(name, \"Hello, \" + name);\n\t}\n\n\tpublic Hello(String name, String greeting) {\n\t\tsuper();\n\t\tthis.name = name;\n\t\tthis.greeting = greeting;\n\t\tthis.currentTime = System.currentTimeMillis();\n\t}\n\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\n\tpublic final void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\tpublic final String getGreeting() {\n\t\treturn greeting;\n\t}\n\n\tpublic final void setGreeting(String greeting) {\n\t\tthis.greeting = greeting;\n\t}\n\n\tpublic final long getCurrentTime() {\n\t\treturn currentTime;\n\t}\n\n\tpublic final void setCurrentTime(long currentTime) {\n\t\tthis.currentTime = currentTime;\n\t}\n\t\n\tpublic final String toString() {\n\t\treturn Strings.toString(this);\n\t}\n\t\n}\n```\n\n这个 Hello 是对消息内容的描述，server 端推送消息时，发的是 Hello 对象，客户端接收消息时也是收的 Hello 对象，这点下面会再讲到。\n\n\n## 创建 demo-reflux-server 项目\n\n然后，我们创建服务端的项目 demo-reflux-server ，并在 pom.xml 文件中添加刚才 demo-reflux 项目的依赖，如：\n\n```xml\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>demo-reflux</artifactId>\n\t\t\t<version>0.0.1-SNAPSHOT</version>\n\t\t</dependency>\n```\n\n由于 demo-reflux 项目中已经添加了 terran4j-commons-reflux 的依赖了，所以这里不需要重复添加。\n\n整个 pom.xml 文件如下所示：\n\n```xml\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<groupId>terran4j</groupId>\n\t<artifactId>demo-reflux-server</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t<packaging>jar</packaging>\n\n\t<name>demo-reflux-server</name>\n\t<url>http://maven.apache.org</url>\n\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>1.5.2.RELEASE</version>\n\t</parent>\n\n\t<properties>\n\t\t<java.version>1.8</java.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>demo-reflux</artifactId>\n\t\t\t<version>0.0.1-SNAPSHOT</version>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n```\n\n然后我们编写 main 函数，并在类上添加 @EnableRefluxServer, 代码如下所示：\n\n```java\npackage com.terran4j.demo.reflux.server;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.reflux.server.EnableRefluxServer;\n\n@EnableRefluxServer\n@SpringBootApplication\npublic class RefluxServerApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(RefluxServerApplication.class, args);\n\t}\n\t\n}\n```\n\n@EnableRefluxServer 会在 Spring 容器中定义很多 Spring Bean 对象，以提供 Reflux 服务端的能力。\n\n我们还要在 application.yml 中定义下所用的端口：\n```yml\nserver:\n  port: 8081\n```\n为了避免端口冲突，demo-reflux-server 项目使用 8081 端口；后面的 demo-reflux-client 项目将使用 8082 端口。\n\n\n## Reflux 服务端 —— 建立 WebSocket 端点\n\n3. 编写  DemoServerEndpoint 类，以建立WebSocket端点，代码如下所示：\n\n```java\npackage com.terran4j.demo.reflux.server;\n\nimport javax.websocket.server.ServerEndpoint;\n\nimport org.springframework.stereotype.Component;\n\nimport com.terran4j.commons.reflux.server.RefluxServerEndpoint;\n\n@ServerEndpoint(\"/demo/connect\")\n@Component\npublic class DemoServerEndpoint extends RefluxServerEndpoint {\n\n\t@Override\n\tprotected boolean authenticate(String clientId) {\n\t\t/**\n\t\t * 这里为了简化演示代码，就不作 clientId 的校验了，直接返回 true 。\n\t\t */\n\t\treturn true;\n\t}\n\n}\n```\n\n说明一下：\n * 必须继承于 RefluxServerEndpoint 类，RefluxServerEndpoint 提供了很多现成的处理 WebSocket 连接的方法。\n * 必须实现方法 `boolean authenticate(String clientId)` 用于校验 clientId 的合法性，这里为了简化演示代码，就不作 clientId 的校验了，直接返回 true 。\n * 类上必须加上 @ServerEndpoint 的注解，用于定义连接 WebSocket 时的请求路径。\n * 类上必须加上 @Component 注解，用于注册成为 Spring Bean。\n \n建立好 WebSocket 端点后，既使客户端不是 java 的（比如： PHP, Javascript, Android, iOS），也可以按 Web Socket 的协议请求连接了。\n\n当然如果是  java ，用 reflux 提供的 client API 就非常简单了，这点后面会讲到。\n\n\n## 创建 demo-reflux-client 项目\n\n现在我们尝试编写 Reflux 客户端项目 demo-reflux-client 。\n\n与服务端一样，也是先创建 demo-reflux-client 项目，也是在 pom.xml 中添加依赖：\n\n```xml\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>demo-reflux</artifactId>\n\t\t\t<version>0.0.1-SNAPSHOT</version>\n\t\t</dependency>\n```\n\n整个 pom.xml 文件与服务端类似，这里就不重复了。\n\n然后我们编写 main 函数，并在类上添加 @EnableRefluxClient, 代码如下所示：\n\n```java\npackage com.terran4j.demo.reflux.client;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.reflux.client.EnableRefluxClient;\n\n@EnableRefluxClient\n@SpringBootApplication\npublic class RefluxClientApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(RefluxClientApplication.class, args);\n\t}\n\n}\n```\n\n@EnableRefluxClient 会在 Spring 容器中定义很多 Spring Bean 对象，以提供 Reflux 客户端的能力。\n\n\n## Reflux 客户端 —— 建立 WebSocket 连接\n\n现在我们来编写一个名为 MyRefluxConnector 的类，来连接服务端：\n\n```java\npackage com.terran4j.demo.reflux.client;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.ApplicationArguments;\nimport org.springframework.boot.ApplicationRunner;\nimport org.springframework.stereotype.Service;\n\nimport com.terran4j.commons.reflux.RefluxClient;\n\n@Service\npublic class MyRefluxConnector implements ApplicationRunner {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(MyRefluxConnector.class);\n\n\t@Value(\"${reflux.server.url}\")\n\tprivate String serverURL;\n\n\t@Value(\"${reflux.client.id}\")\n\tprivate String clientId;\n\n\t@Autowired\n\tprivate RefluxClient refluxClient;\n\n\t@Override\n\tpublic void run(ApplicationArguments args) throws Exception {\n\t\t// 建立 Web Socket 连接。\n\t\trefluxClient.connect(serverURL, clientId);\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"connect server success, serverURL = {}, clientId = {}\", //\n\t\t\t\t\tserverURL, clientId);\n\t\t}\n\t}\n\n}\n```\n\n其实主要就两步：\n\n * 第一步，用 @Autowired 的方式注入 Bean: RefluxClient refluxClient 。\n * 第二步，调用 `refluxClient.connect(serverURL, clientId)` 方法建立连接。\n\n就这么简单，serverURL 是一个 WebSocket 的URL，比如在本例中，它的值为：\n\n```\nws://localhost:8081/demo/connect\n```\n\nws 是 WebSocket 协议的意思，之所示路径是 `/demo/connect` ，是在服务端 `DemoServerEndpoint` 类中定义的，如：\n\n```java\n@ServerEndpoint(\"/demo/connect\")\n@Component\npublic class DemoServerEndpoint extends RefluxServerEndpoint\n```\n\n为了能在客户端程序启动时就连接 WebSocket，这个类实现了 `ApplicationRunner` 接口并将代码放在 run 方法中，当然这只是代码演示的方便，您可以根据实际业务的需求，选择什么时机进行连接。\n\n好了，我们可以分别启动 RefluxServerApplication, RefluxClientApplication 两个 main 函数来看看效果了。\nclient端、server端两个程序都启动后，看到 client 端的控制台输出如下：\n```\n......\n2017-08-20 16:05:24.786  INFO 8564 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8082 (http)\n2017-08-20 16:05:24.791  INFO 8564 --- [           main] c.t.c.reflux.client.RefluxClientImpl     : connect server web socket url: ws://localhost:8081/demo/connect?clientId=12345\n2017-08-20 16:05:25.094  INFO 8564 --- [           main] c.t.c.reflux.client.ClientConnection     : Opening client websocket, server = ws://localhost:8081/demo/connect\n2017-08-20 16:05:25.098  INFO 8564 --- [           main] c.t.c.reflux.client.RefluxClientImpl     : 目标服务连接成功： ws://localhost:8081/demo/connect?clientId=12345\n2017-08-20 16:05:25.100  INFO 8564 --- [           main] c.t.d.reflux.client.MyRefluxConnector    : connect server success, serverURL = ws://localhost:8081/demo/connect, clientId = 12345\n2017-08-20 16:05:25.103  INFO 8564 --- [           main] c.t.d.r.client.RefluxClientApplication   : Started RefluxClientApplication in 4.605 seconds (JVM running for 5.146)\n```\n\nserver 端的控制台输出如下：\n\n```\n......\n2017-08-20 16:05:05.855  INFO 7052 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)\n2017-08-20 16:05:05.862  INFO 7052 --- [           main] c.t.d.r.server.RefluxServerApplication   : Started RefluxServerApplication in 4.986 seconds (JVM running for 5.583)\n2017-08-20 16:05:24.998  INFO 7052 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'\n2017-08-20 16:05:24.999  INFO 7052 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started\n2017-08-20 16:05:25.024  INFO 7052 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 25 ms\n2017-08-20 16:05:25.106  INFO 7052 --- [nio-8081-exec-1] c.t.c.r.server.RefluxServerEndpoint      : 来自客户端的连接, clientId = 12345\n2017-08-20 16:05:25.108  INFO 7052 --- [nio-8081-exec-1] c.t.c.reflux.server.RefluxServerImpl     : 有新连接加入! 当前连接数为 1\n```\n\n说明连接成功了。\n\n\n## 服务端推送消息到客户端\n\n建立连接后，服务端可以主动推送消息了，我们编写 DemoController 类来实现消息推送，如下所示：\n\n```java\npackage com.terran4j.demo.reflux.server;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RequestParam;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport com.terran4j.commons.reflux.RefluxServer;\nimport com.terran4j.demo.reflux.Hello;\n\n@RestController\npublic class DemoController {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(DemoController.class);\n\n\t@Autowired\n\tprivate RefluxServer refluxServer;\n\n\t@RequestMapping(value = \"/demo/send\", method = RequestMethod.GET)\n\tpublic String sendHello(@RequestParam(\"name\") String name) {\n\t\tHello hello = new Hello(name);\n\t\trefluxServer.sendAll(hello);\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"send hello message to ALL client done:\\n{}\", //\n\t\t\t\t\thello);\n\t\t}\n\t\treturn \"success\";\n\t}\n\n}\n```\n\n其实主要就两步：\n * 第一步， 用 @Autowired 注入 Bean: RefluxServer refluxServer 。\n * 第二步，调用方法 `refluxServer.sendAll(hello);` 发送消息，消息内容是自定义的任意 Java Bean 对象（如这里的Hello hello）。\n\nRefluxServer 提供了两个推送消息的方法，一个是 `int sendAll(Object content)` 推送消息到所有已建立连接的客户端，它返回 int 值表示推送成功的连接数量；\n还有一个是 `boolean send(Object content, String clientId)` 方法，它会定向推送消息到指定 clientId 的某个客户端，它返回 boolean 值表示是否推送成功。\n\n\n同时客户端需要编写代码以接收消息，我们在 demo-reflux-client 项目中编写 MyRefluxReceiver 类来接收消息，代码如下所示：\n\n```java\npackage com.terran4j.demo.reflux.client;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Service;\n\nimport com.terran4j.commons.reflux.OnMessage;\nimport com.terran4j.demo.reflux.Hello;\n\n@Service\npublic class MyRefluxReceiver {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(MyRefluxReceiver.class);\n\n\t@OnMessage\n\tpublic void onHello(Hello hello) {\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"receive message hello:\\n{}\", hello);\n\t\t}\n\t}\n}\n```\n\n需要注意以下几点：\n\n * 在一个 Spring Bean 类中编写一个用于接受消息的方法，**并加上 @OnMessage 注解**，如上面的 onHello 方法。\n * 此方法的**入参有且仅有一个**，类型必需与发消息时一样的类，若发消息时的消息类与收消息时的消息类不一样的话，就会接收不到。\n * 此方法的**返回类型只是能 void 或 String 类型的**，void 表示无返回内容， String 表示有返回内容（通过WebSocket返回到服务端）。\n\n\n最后，我们将 client 与 server 同时启动，然后在浏览器中输入 URL ：\n\n```\nhttp://localhost:8081/demo/send?name=terran4j\n```\n\n这会调用服务端的这个方法：\n\n```java\n\t@RequestMapping(value = \"/demo/send\", method = RequestMethod.GET)\n\tpublic String sendHello(@RequestParam(\"name\") String name) {\n\t\tHello hello = new Hello(name);\n\t\trefluxServer.sendAll(hello);\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"send hello message to ALL client done:\\n{}\", //\n\t\t\t\t\thello);\n\t\t}\n\t\treturn \"success\";\n\t}\n```\n\n服务端控制台的输出如下所示：\n\n```\n2017-08-20 16:42:58.031  INFO 15712 --- [nio-8081-exec-2] c.t.demo.reflux.server.DemoController    : send hello message to ALL client done:\n{\n  \"name\" : \"terran4j\",\n  \"greeting\" : \"Hello, terran4j\",\n  \"currentTime\" : 1503218577967\n}\n```\n\n而在客户端，Reflux 框架收到消息后也会调用下面这个 onHello 方法：\n\n```java\n\t@OnMessage\n\tpublic void onHello(Hello hello) {\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"receive message hello:\\n{}\", hello);\n\t\t}\n\t}\n```\n\n结果客户端控制台输出如下：\n\n```\n2017-08-20 16:42:58.055  INFO 7848 --- [lient-AsyncIO-1] c.t.demo.reflux.client.MyRefluxReceiver  : receive message hello:\n{\n  \"name\" : \"terran4j\",\n  \"greeting\" : \"Hello, terran4j\",\n  \"currentTime\" : 1503218577967\n}\n```\n\n说明消息的推送和接收都成功了。\n\n\n## 分布式解决方案\n\n目前讲的消息推送一直是在单机下进行的，如果服务端是有多台实例的分布式环境呢？\n\n在分布式的环境下，会出现以下情况：\n1. 服务端有多台实例，并且都是同构的。\n2. 服务端实例个数是可能会动态增加或减少（但某台实例当机不应该影响服务的可用性）。\n3. 某个客户端连接了其中一台服务端实例，但执行“向这个客户端推送消息”的是另一个实例。\n\n一个有效的解决方案就是使用具有“发布-订阅”机制的消息中间件，比如： RabbitMQ, Kafka 等，也可以用 Redis 提供的“发布-订阅”功能。\n具体来说，就是执行定向推送消息的实例，如果发现目标客户端连接的并不是自己，就发到消息中间件上，其它实例都在监听这一topic，如果此客户端是连接到本实例中，就执行推送，否则就忽略之。\n另外，客户端内部有一个守护线程，轮询检查与服务端的连接是否中断，中断的话就重新请求连接。\n\n至于是选  RabbitMQ, Kafka, 还是 Redis，则根据业务需求而定：\n\n * 业务上要求稳定，不能丢消息的场景下，建议用 RabbitMQ 。\n * 业务上要求超大并发、高吞吐量的场景下，建议用 Kafka 。\n * 业务上要求高实时、低延迟的场景下，建议用 Redis 。\n\n由于需要根据业务场景而定，对分布式的支持的功能并未包含在本项目中，但根据以上的分析，在 Reflux 的基础上自行实现并不复杂，建议广大开发者发挥自己的聪明才智自行解决，更欢迎向本项目贡献代码。\n\n\n## 资源分享与技术交流\n\n如果你觉得本项目对你有用的话，希望可以定期收到更多分享的精彩技术干货，或者希望与笔者交流相关技术问题，可以加一下我们的 **SpringBoot及微服务** 微信公众号，请拿起手机扫描下面的二维码关注下吧！\n![SpringBoot及微服务-公众号二维码](http://upload-images.jianshu.io/upload_images/4489584-f4f91efb322bd92c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)"
  },
  {
    "path": "commons-reflux/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<parent>\n\t\t<groupId>com.github.terran4j</groupId>\n\t\t<artifactId>terran4j-commons-parent</artifactId>\n\t\t<version>1.0.4-SNAPSHOT</version>\n\t</parent>\n\n\t<artifactId>terran4j-commons-reflux</artifactId>\n\t<packaging>jar</packaging>\n\t<name>terran4j-commons-reflux</name>\n\t<url>https://github.com/terran4j/commons</url>\n\n\t<dependencies>\n\n\t\t<!-- terran4j 工具类库。 -->\n\t\t<dependency>\n\t\t\t<groupId>com.github.terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-util</artifactId>\n\t\t</dependency>\n\n\t\t<!-- json -->\n\t\t<dependency>\n\t\t\t<groupId>com.google.code.gson</groupId>\n\t\t\t<artifactId>gson</artifactId>\n\t\t</dependency>\n\t\t\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-aop</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework</groupId>\n\t\t\t<artifactId>spring-aspects</artifactId>\n\t\t</dependency>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-websocket</artifactId>\n\t\t</dependency>\n\t\t\n\t</dependencies>\n\t\n</project>\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/EnableReflux.java",
    "content": "package com.terran4j.commons.reflux;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.springframework.context.annotation.Import;\n\nimport com.terran4j.commons.reflux.client.RefluxClientConfiguration;\nimport com.terran4j.commons.reflux.server.RefluxServerConfiguration;\n\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@Import({ //\n\t\tRefluxClientConfiguration.class, //\n\t\tRefluxServerConfiguration.class //\n})\npublic @interface EnableReflux {\n\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/Message.java",
    "content": "package com.terran4j.commons.reflux;\n\nimport java.util.UUID;\n\n/**\n * \n * \n * @author wei.jiang\n *\n */\npublic class Message {\n\t\n\tpublic static final int STATUS_REQUEST = -1;\n\t\n\tpublic static final int STATUS_REPLY_SUCCESS = 0;\n\t\n\tpublic static final String generateId() {\n\t\treturn UUID.randomUUID().toString();\n\t}\n\n\t/**\n\t * 消息的id，具有唯一性。\n\t */\n\tprivate String id;\n\n\t/**\n\t * 消息的状态：-1 表示请求； 0 表示成功响应； > 1 表示错误码。\n\t */\n\tprivate int status;\n\n\t/**\n\t * 消息的类型，在一个服务内唯一。\n\t */\n\tprivate String type;\n\n\t/**\n\t * 消息的内容，json串格式。\n\t */\n\tprivate Object content;\n\n\t/**\n\t * @return the id\n\t */\n\tpublic final String getId() {\n\t\treturn id;\n\t}\n\n\t/**\n\t * @param id the id to set\n\t */\n\tpublic final void setId(String id) {\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * @return the type\n\t */\n\tpublic final String getType() {\n\t\treturn type;\n\t}\n\n\t/**\n\t * @param type the type to set\n\t */\n\tpublic final void setType(String type) {\n\t\tthis.type = type;\n\t}\n\n\t/**\n\t * @return the content\n\t */\n\tpublic final Object getContent() {\n\t\treturn content;\n\t}\n\n\t/**\n\t * @param content the content to set\n\t */\n\tpublic final void setContent(Object content) {\n\t\tthis.content = content;\n\t}\n\n\t/**\n\t * @return the status\n\t */\n\tpublic final int getStatus() {\n\t\treturn status;\n\t}\n\n\t/**\n\t * @param status the status to set\n\t */\n\tpublic final void setStatus(int status) {\n\t\tthis.status = status;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/OnMessage.java",
    "content": "package com.terran4j.commons.reflux;\n\nimport static java.lang.annotation.ElementType.METHOD;\nimport static java.lang.annotation.ElementType.TYPE;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\n/**\n * 用于客户端，接收来自服务端的消息推送。<br>\n * 修饰在一个Spring Bean的方法上，要求此方法有且仅有一个参数，表示用此参数进行反序列化接受消息。<br>\n * 如果此方法有返回值，会将此返回值序列化成消息发给服务端。<br>\n * 也可以没有返回值。\n * \n * @author wei.jiang\n *\n */\n@Documented\n@Retention(RUNTIME)\n@Target({ TYPE, METHOD })\npublic @interface OnMessage {\n\n\t/**\n\t * 消息类型的标识符，对于同一个服务端应用，此标识符不允许重复。\n\t * \n\t * @return\n\t */\n\tString type() default \"\";\n\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/RefluxClient.java",
    "content": "package com.terran4j.commons.reflux;\n\nimport com.terran4j.commons.util.error.BusinessException;\n\npublic interface RefluxClient {\n\n\tboolean connect(String serverURL, String clientId) throws BusinessException;\n\t\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/RefluxErrorCode.java",
    "content": "package com.terran4j.commons.reflux;\n\nimport com.terran4j.commons.util.error.ErrorCode;\n\npublic enum RefluxErrorCode implements ErrorCode {\n\t\n\tCLIENT_NOT_FOUND(2, \"client.not.found\"),\n\t\n\tCLIENT_AUTH_FAILED(3, \"client.auth.failed\"),\n\t\n\tCLIENT_CONNECTION_FULL(5, \"client.connection.full\"),\n\t\n\tNOT_AUTHED(6, \"not.authed\"),\n\t\n\tCLIENT_ID_USED_CONCURRENTLY(7, \"clientId.used.concurrently\"),\n\t\n\t;\n\t\n\n\tprivate final int value;\n\t\n\tprivate final String name;\n\n\tprivate RefluxErrorCode(int value, String name) {\n\t\tthis.value = value;\n\t\tthis.name = name;\n\t}\n\n\tpublic final int getValue() {\n\t\treturn value;\n\t}\n\t\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/RefluxServer.java",
    "content": "package com.terran4j.commons.reflux;\n\n/**\n * 连接管理器，用于服务端管理所有的客户端连接通道。<br>\n * 注意：暂时只用于服务端，用于服务端向客户端的逆向推送。<br>\n * TODO: 后续会考虑要不要加上正向推送。<br>\n * \n * @author wei.jiang\n *\n */\npublic interface RefluxServer {\n\t\n\t/**\n\t * 是否有这个客户端的连接。\n\t * @param clientId\n\t * @return\n\t */\n\tboolean isConnected(String clientId);\n\t\n\t/**\n\t * 默认的推送方式，特点如下：<br>\n\t * 1. 只推送消息，不要返回内容。<br>\n\t * 2. 采用尽力而为的送达方式，并且不保证同一个时间点上执行。<br>\n\t * 具体来说，就是推送成功的实例，就立即执行客户端的响应函数；<br>\n\t * 推送不成功的实例，会重试几次，重试全部都不成功就放弃了。<br>\n\t * 3. 如果有多个客户端实例，会全部推送，而不是选其中若干台推送。<br>\n\t * \n\t * @param content\n\t *            消息内容。\n\t */\n\t<T> int sendAll(T content);\n\t\n\t/**\n\t * 与 sendAll 类似，唯一不同的是只向一个客户端推送消息（而不是所有客户端）。\n\t * \n\t * @param content 消息内容。\n\t * @param clientId 客戶端 id 。\n\t * @return\n\t */\n\t<T> boolean send(T content, String clientId);\n\n//\t/**\n//\t * 自定义的推送方式，通过参数设置推送的具体方式。<br>\n//\t * TODO: 考虑本方法比较复杂，目前暂不实现。\n//\t * @param message\n//\t *            消息内容。\n//\t * @param replyClass\n//\t *            返回内容，如果没有返回内容就设置成<code>java.lang.Void</code>类型。\n//\t * @param instanceCount\n//\t *            如果为 0 或负数，表示全部实例都要推送，如果是正整数，表示只推送指定数量的实例。\n//\t * @param ensureDelivered\n//\t *            是否以确保同时送达: <br>\n//\t *            如果为true，会保证消息送达到每个实例上，并且每个实例会同时执行响应函数，如果做不到这一点，就都全部不执行。<br>\n//\t *            如果为false，会采用尽力而为的送达方式，送达到了就立即执行响应函数，不成功就重试，重试几次都不成功就放弃。<br>\n//\t * @return\n//\t */\n//\t<T, V> List<Reply<V>> send(T message, Class<V> replyClass, int instanceCount, boolean ensureDelivered);\n\n}"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/Reply.java",
    "content": "package com.terran4j.commons.reflux;\n\n/**\n * 客户端的应答内容。\n * \n * @author wei.jiang\n */\npublic class Reply<T> {\n\n\tprivate T data;\n\t\n\tprivate String clientHost;\n\t\n\tprivate String clientIP;\n\t\n\tprivate long requestTime;\n\t\n\tprivate long responseTime;\n\t\n\tprivate long resultCode;\n\t\n\tprivate String resultName;\n\t\n\tprivate String message;\n\n\t/**\n\t * @return the data\n\t */\n\tpublic final T getData() {\n\t\treturn data;\n\t}\n\n\t/**\n\t * @param data the data to set\n\t */\n\tpublic final void setData(T data) {\n\t\tthis.data = data;\n\t}\n\n\t/**\n\t * @return the clientHost\n\t */\n\tpublic final String getClientHost() {\n\t\treturn clientHost;\n\t}\n\n\t/**\n\t * @param clientHost the clientHost to set\n\t */\n\tpublic final void setClientHost(String clientHost) {\n\t\tthis.clientHost = clientHost;\n\t}\n\n\t/**\n\t * @return the clientIP\n\t */\n\tpublic final String getClientIP() {\n\t\treturn clientIP;\n\t}\n\n\t/**\n\t * @param clientIP the clientIP to set\n\t */\n\tpublic final void setClientIP(String clientIP) {\n\t\tthis.clientIP = clientIP;\n\t}\n\n\t/**\n\t * @return the requestTime\n\t */\n\tpublic final long getRequestTime() {\n\t\treturn requestTime;\n\t}\n\n\t/**\n\t * @param requestTime the requestTime to set\n\t */\n\tpublic final void setRequestTime(long requestTime) {\n\t\tthis.requestTime = requestTime;\n\t}\n\n\t/**\n\t * @return the responseTime\n\t */\n\tpublic final long getResponseTime() {\n\t\treturn responseTime;\n\t}\n\n\t/**\n\t * @param responseTime the responseTime to set\n\t */\n\tpublic final void setResponseTime(long responseTime) {\n\t\tthis.responseTime = responseTime;\n\t}\n\n\t/**\n\t * @return the resultCode\n\t */\n\tpublic final long getResultCode() {\n\t\treturn resultCode;\n\t}\n\n\t/**\n\t * @param resultCode the resultCode to set\n\t */\n\tpublic final void setResultCode(long resultCode) {\n\t\tthis.resultCode = resultCode;\n\t}\n\n\t/**\n\t * @return the resultName\n\t */\n\tpublic final String getResultName() {\n\t\treturn resultName;\n\t}\n\n\t/**\n\t * @param resultName the resultName to set\n\t */\n\tpublic final void setResultName(String resultName) {\n\t\tthis.resultName = resultName;\n\t}\n\n\t/**\n\t * @return the message\n\t */\n\tpublic final String getMessage() {\n\t\treturn message;\n\t}\n\n\t/**\n\t * @param message the message to set\n\t */\n\tpublic final void setMessage(String message) {\n\t\tthis.message = message;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/client/ClientConnection.java",
    "content": "package com.terran4j.commons.reflux.client;\n\nimport java.io.IOException;\n\nimport javax.websocket.ClientEndpoint;\nimport javax.websocket.CloseReason;\nimport javax.websocket.OnClose;\nimport javax.websocket.OnMessage;\nimport javax.websocket.OnOpen;\nimport javax.websocket.Session;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.google.gson.JsonSyntaxException;\nimport com.terran4j.commons.util.error.BusinessException;\n\n/**\n * 客户端的连接。\n * @author wei.jiang\n */\n@ClientEndpoint\npublic class ClientConnection {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(ClientConnection.class);\n\t\n\tprivate final String serverURL;\n\t\n\tprotected final String clientId;\n\n\tprivate final MessageHandler messageHandler;\n\n\tprivate Session session = null;\n\n\tpublic ClientConnection(String serverURL, String clientId, MessageHandler messageHandler) {\n\t\tsuper();\n\t\tthis.serverURL = serverURL;\n\t\tthis.clientId = clientId;\n\t\tthis.messageHandler = messageHandler;\n\t}\n\n\tpublic boolean isOpen() {\n\t\treturn session != null && session.isOpen();\n\t}\n\n\t@OnOpen\n\tpublic void onOpen(Session userSession) {\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"Opening client websocket, server = {}\", serverURL);\n\t\t}\n\t\tsession = userSession;\n\t}\n\n\t@OnClose\n\tpublic void onClose(Session userSession, CloseReason reason) {\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"Closing client websocket, server = {}, reason: {}\", //\n\t\t\t\t\tserverURL, reason.toString());\n\t\t}\n\t\tsession = null;\n\t}\n\n\t/**\n\t * @param message\n\t */\n\t@OnMessage\n\tpublic void onMessage(String message) {\n\t\tString reply = null;\n\t\ttry {\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"receive message from {}, message:\\n{}\", serverURL, message);\n\t\t\t}\n\t\t\treply = messageHandler.onMessage(message);\n\t\t} catch (JsonSyntaxException e) {\n\t\t\tlog.error(\"Can't parse message: \\n{}\", message, e);\n\t\t} catch (BusinessException e) {\n\t\t\te.printStackTrace();\n\t\t\t// TODO: 异常处理。\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tif (reply != null) {\n\t\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\t\tlog.info(\"reply to {}: \\n{}\", serverURL, reply);\n\t\t\t\t}\n\t\t\t\tsendMessage(reply);\n\t\t\t}\n\t\t} catch (IOException e) {\n\t\t\tlog.error(\"reply message failed: \\n{}\", reply, e);\n\t\t}\n\t}\n\n\tpublic void sendMessage(String message) throws IOException {\n\t\tif (session != null && session.isOpen()) {\n\t\t\tsynchronized (session) {\n\t\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\t\tlog.info(\"sendMessage: \\n{}\", message);\n\t\t\t\t}\n\t\t\t\tsession.getAsyncRemote().sendText(message);\n\t\t\t}\n\t\t}\n\t}\n\t\n\tpublic void close() throws IOException {\n\t\tif (session != null && session.isOpen()) {\n\t\t\tsession.close();\n\t\t}\n\t}\n\n\tpublic final MessageHandler getMessageHandler() {\n\t\treturn messageHandler;\n\t}\n\n\tpublic final String getServerURL() {\n\t\treturn serverURL;\n\t}\n\n\tpublic final String getClientId() {\n\t\treturn clientId;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/client/EnableRefluxClient.java",
    "content": "package com.terran4j.commons.reflux.client;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.springframework.context.annotation.Import;\n\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@Import(RefluxClientConfiguration.class)\npublic @interface EnableRefluxClient {\n\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/client/MessageHandler.java",
    "content": "package com.terran4j.commons.reflux.client;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.terran4j.commons.reflux.Message;\nimport com.terran4j.commons.reflux.OnMessage;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.config.BeanPostProcessor;\nimport org.springframework.beans.factory.support.BeanDefinitionValidationException;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n@Service\npublic class MessageHandler implements BeanPostProcessor {\n\n    private static final JsonParser jsonParser = new JsonParser();\n\n    private static final Gson gson = new Gson();\n\n    private static final Map<String, OnMessageInvoker> invokers = new ConcurrentHashMap<>();\n\n    private static class OnMessageInvoker {\n\n        public Method method;\n\n        public Object bean;\n\n        public Class<?> paramType;\n    }\n\n    @Override\n    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {\n        return bean;\n    }\n\n    @Override\n    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {\n        Class<?> targetClass = Classes.getTargetClass(bean);\n\n        Method[] methods = Classes.getMethods(OnMessage.class, targetClass);\n        if (methods != null && methods.length > 0) {\n            for (Method method : methods) {\n                OnMessage onMessage = method.getAnnotation(OnMessage.class);\n\n                Class<?>[] paramTypes = method.getParameterTypes();\n                if (paramTypes == null || paramTypes.length != 1) {\n                    throw new BeanDefinitionValidationException(\"@OnMessage修饰的方法，必须有且仅有一个参数\");\n                }\n                Class<?> paramType = paramTypes[0];\n\n                String msgType = onMessage.type();\n                if (StringUtils.isEmpty(msgType)) {\n                    msgType = paramType.getName();\n                }\n\n                Class<?> returnType = method.getReturnType();\n                if (returnType != null && !(\"void\".equals(returnType.getName()) || String.class.equals(returnType))) {\n                    throw new BeanDefinitionValidationException(\"@OnMessage修饰的方法，返回类型只能是 String 或 void 。\");\n                }\n\n                String key = msgType;\n                OnMessageInvoker invoker = new OnMessageInvoker();\n                invoker.method = method;\n                invoker.bean = bean;\n                invoker.paramType = paramType;\n                invokers.put(key, invoker);\n            }\n        }\n        return bean;\n    }\n\n    public String onMessage(String message) throws BusinessException {\n        JsonElement element = jsonParser.parse(message);\n        JsonObject json = element.getAsJsonObject();\n\n        Message result = new Message();\n\n        String msgId = json.get(\"id\").getAsString();\n        result.setId(msgId);\n\n        String msgType = json.get(\"type\").getAsString();\n        result.setType(msgType);\n\n        OnMessageInvoker invoker = invokers.get(msgType);\n        if (invoker == null) {\n            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND) //\n                    .put(\"msgType\", msgType);\n        }\n\n        String msgContent = json.get(\"content\").toString();\n        Object param = gson.fromJson(msgContent, invoker.paramType);\n\n        Object reply = null;\n        try {\n            reply = invoker.method.invoke(invoker.bean, param);\n        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {\n            throw new BusinessException(CommonErrorCode.INTERNAL_ERROR, e) //\n                    .put(\"msgType\", msgType).put(\"msgContent\", msgContent);\n        }\n        if (reply == null) {\n            return null;\n        }\n\n        String content = reply.toString();\n        result.setContent(content);\n        result.setStatus(0);\n\n        return gson.toJson(result);\n    }\n\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/client/RefluxClientConfiguration.java",
    "content": "package com.terran4j.commons.reflux.client;\n\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\n\n@ComponentScan(basePackageClasses = { RefluxClientImpl.class })\n@Configuration\npublic class RefluxClientConfiguration {\n\n\t\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/client/RefluxClientImpl.java",
    "content": "package com.terran4j.commons.reflux.client;\n\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URLEncoder;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport javax.annotation.PostConstruct;\nimport javax.websocket.ContainerProvider;\nimport javax.websocket.DeploymentException;\nimport javax.websocket.Session;\nimport javax.websocket.WebSocketContainer;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.stereotype.Service;\n\nimport com.terran4j.commons.reflux.RefluxClient;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\nimport com.terran4j.commons.util.task.LoopExecuteTask;\n\n/**\n * 客户端连接管理。\n * \n * @author wei.jiang\n *\n */\n@Service\npublic class RefluxClientImpl extends LoopExecuteTask implements RefluxClient {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(RefluxClientImpl.class);\n\n\tprivate static final WebSocketContainer container = ContainerProvider.getWebSocketContainer();\n\n\tprivate static final Map<String, ClientConnection> connections = new ConcurrentHashMap<>();\n\n\tstatic final Map<String, ClientConnection> getConnections() {\n\t\treturn connections;\n\t}\n\n\t@Value(\"${comm.connect.interval:10000}\")\n\tprivate long connectInterval;\n\n\t@Autowired\n\tprivate MessageHandler messageHandler;\n\n\t@Autowired\n\tprivate ApplicationContext context;\n\n\tprivate Thread connectionRecoverThread = null;\n\n\tprivate volatile boolean inited = false;\n\n\t@PostConstruct\n\tpublic void init() throws BusinessException {\n\t\tif (inited) {\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"ClientConnectionService is inited, no need init again.\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tsynchronized (this) {\n\t\t\tif (inited) {\n\t\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\t\tlog.info(\"ClientConnectionService is inited, no need init again.\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"start to init ClientConnectionService.\");\n\t\t\t}\n\n\t\t\tsetSleepTime(connectInterval);\n\n\t\t\t// 启动连接自动恢复线程。\n\t\t\tconnectionRecoverThread = new Thread(this);\n\t\t\tconnectionRecoverThread.setDaemon(true);\n\t\t\tconnectionRecoverThread.setName(\"Connection Recover Thread\");\n\t\t\tconnectionRecoverThread.start();\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"自动恢复线程已启动。\");\n\t\t\t}\n\n\t\t\tinited = true;\n\t\t}\n\t}\n\n\t@Override\n\tprotected boolean execute() throws Exception {\n\t\treconnectAll();\n\t\treturn false;\n\t}\n\n\tboolean reconnectAll() {\n\n\t\t// 先过滤掉已经连上的连接。\n\t\tList<ClientConnection> targets = new ArrayList<>();\n\t\tCollection<ClientConnection> conns = connections.values();\n\t\tfor (ClientConnection endpoint : conns) {\n\t\t\tif (endpoint != null && !endpoint.isOpen()) {\n\t\t\t\ttargets.add(endpoint);\n\t\t\t}\n\t\t}\n\t\tif (targets.size() == 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\tStringBuilder info = new StringBuilder();\n\t\tint failedCount = 0;\n\t\tinfo.append(\"尝试重新连接服务端：\\n\");\n\t\tfor (int i = 0; i < targets.size(); i++) {\n\t\t\tClientConnection conn = targets.get(i);\n\t\t\tString server = conn.getServerURL();\n\t\t\tString clientId = conn.getClientId();\n\t\t\tboolean success = true;\n\t\t\ttry {\n\t\t\t\tsuccess = connect(server, clientId);\n\t\t\t} catch (BusinessException e) {\n\t\t\t\tlog.error(\"connect server[\" + server + \"] failed: \" + e.getMessage(), e);\n\t\t\t\tsuccess = false;\n\t\t\t}\n\t\t\tinfo.append(\"    连接服务端 \").append(server).append(\" ： \");\n\t\t\tinfo.append(success ? \"成功\" : \"失败\").append(\"\\n\");\n\t\t\tif (!success) {\n\t\t\t\tfailedCount++;\n\t\t\t}\n\t\t}\n\n\t\tif (failedCount == 0) {\n\t\t\tinfo.append(\"全部服务端都已连接上，共重新连接上\").append(targets.size()).append(\"个服务端.\");\n\t\t} else {\n\t\t\tinfo.append(\"连接结束，有\").append(failedCount).append(\"个服务端未能连接上，将稍后重试！\");\n\t\t}\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(info.toString());\n\t\t}\n\n\t\treturn failedCount == 0;\n\t}\n\n\t@Override\n\tpublic final boolean connect(String serverURL, String clientId) throws BusinessException {\n\n\t\ttry {\n\t\t\tURLEncoder.encode(clientId, \"UTF-8\");\n\t\t} catch (UnsupportedEncodingException e1) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INVALID_PARAM, e1) //\n\t\t\t\t\t.setMessage(\"参数 clientId 不能进行 URLEncoder 编码： ${clientId}\").put(\"clientId\", clientId);\n\t\t}\n\t\tString url = new StringBuffer(serverURL).append(\"?clientId=\").append(clientId).toString();\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"connect server web socket url: {}\", url);\n\t\t}\n\t\tURI endpointURI = null;\n\t\ttry {\n\t\t\tendpointURI = new URI(url);\n\t\t} catch (URISyntaxException e) {\n\t\t\tthrow new RuntimeException(\"连接的URL不正确： \" + url, e);\n\t\t}\n\n\t\t// 建立 Web Socket 连接。\n\t\tClientConnection oldEndpoint = connections.get(serverURL);\n\t\tClientConnection endpoint = new ClientConnection(serverURL, clientId, messageHandler);\n\t\ttry {\n\t\t\tSession sesson = container.connectToServer(endpoint, endpointURI);\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"目标服务连接成功： {}\", url);\n\t\t\t}\n\t\t\treturn sesson != null;\n\t\t} catch (DeploymentException e) {\n\t\t\tlog.error(\"目标服务部署问题, url = \" + url, e);\n\t\t} catch (IOException e) {\n\t\t\tlog.error(\"目标服务连接问题, url = \" + url, e);\n\t\t} finally {\n\t\t\tconnections.put(serverURL, endpoint);\n\t\t}\n\n\t\t// 如果有旧连接，则释放旧连接。\n\t\tif (oldEndpoint != null) {\n\t\t\ttry {\n\t\t\t\toldEndpoint.close();\n\t\t\t} catch (IOException e) {\n\t\t\t\tlog.error(\"关闭旧连接失败： \" + e.getMessage(), e);\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic long getConnectInterval() {\n\t\treturn connectInterval;\n\t}\n\n\tApplicationContext getContext() {\n\t\treturn context;\n\t}\n\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/server/ClientAppInfo.java",
    "content": "package com.terran4j.commons.reflux.server;\n\npublic class ClientAppInfo {\n\n\tprivate String appKey;\n\t\n\tprivate int maxConnectionCount = 1;\n\t\n\tpublic int getMaxConnectionCount() {\n\t\treturn maxConnectionCount;\n\t}\n\n\tpublic void setMaxConnectionCount(int maxConnectionCount) {\n\t\tthis.maxConnectionCount = maxConnectionCount;\n\t}\n\n\tpublic final String getAppKey() {\n\t\treturn appKey;\n\t}\n\n\tpublic final void setAppKey(String appKey) {\n\t\tthis.appKey = appKey;\n\t}\n\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/server/ClientConnectionInfo.java",
    "content": "package com.terran4j.commons.reflux.server;\n\npublic final class ClientConnectionInfo {\n\n\tprivate String envName;\n\n\tprivate String clientId;\n\n\tprivate long connectedTime;\n\n\tprivate RefluxServerEndpoint connection;\n\n\t/**\n\t * @return the envName\n\t */\n\tpublic final String getEnvName() {\n\t\treturn envName;\n\t}\n\n\t/**\n\t * @param envName\n\t *            the envName to set\n\t */\n\tpublic final void setEnvName(String envName) {\n\t\tthis.envName = envName;\n\t}\n\n\t/**\n\t * @return the clientId\n\t */\n\tpublic final String getClientId() {\n\t\treturn clientId;\n\t}\n\n\t/**\n\t * @param clientId\n\t *            the clientId to set\n\t */\n\tpublic final void setClientId(String clientId) {\n\t\tthis.clientId = clientId;\n\t}\n\n\t/**\n\t * @return the connectedTime\n\t */\n\tpublic final long getConnectedTime() {\n\t\treturn connectedTime;\n\t}\n\n\t/**\n\t * @param connectedTime\n\t *            the connectedTime to set\n\t */\n\tpublic final void setConnectedTime(long connectedTime) {\n\t\tthis.connectedTime = connectedTime;\n\t}\n\n\t/**\n\t * @return the connection\n\t */\n\tpublic final RefluxServerEndpoint getConnection() {\n\t\treturn connection;\n\t}\n\n\t/**\n\t * @param connection\n\t *            the connection to set\n\t */\n\tpublic final void setConnection(RefluxServerEndpoint connection) {\n\t\tthis.connection = connection;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn \"ClientConnectionInfo [envName=\" + envName + \", clientId=\" + clientId + \", connectedTime=\" + connectedTime\n\t\t\t\t+ \"]\";\n\t}\n\n}"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/server/EnableRefluxServer.java",
    "content": "package com.terran4j.commons.reflux.server;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport org.springframework.context.annotation.Import;\n\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@Import(RefluxServerConfiguration.class)\npublic @interface EnableRefluxServer {\n\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/server/RefluxServerConfiguration.java",
    "content": "package com.terran4j.commons.reflux.server;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.socket.server.standard.ServerEndpointExporter;\n\n@ComponentScan(basePackageClasses = { RefluxServerImpl.class })\n@Configuration\npublic class RefluxServerConfiguration {\n\n\t@Bean\n\tpublic ServerEndpointExporter serverEndpointExporter() {\n\t\treturn new ServerEndpointExporter();\n\t}\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/server/RefluxServerEndpoint.java",
    "content": "package com.terran4j.commons.reflux.server;\n\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\n\nimport javax.websocket.*;\nimport javax.websocket.RemoteEndpoint.Async;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.util.List;\n\n/**\n * 客户端应用与服务端应用的连接，可以让服务端向客户端推送消息。<br>\n * 注意： 客户端应用不一定只有一台实例，比如互联网应用一般都有多台同构的实例，每台实例都会连接服务端应用。<br>\n * 同样，服务端应用也不一定只一台实例，每台实例也会被不同的客户端实例所连接。<br>\n * 所以，这里说的“连接通道”，是一个虚拟的概念，指服务端集群与客户端集群建立的连接，屏蔽掉底层实例间连接的细节。<br>\n *\n * @author jiangwei\n */\npublic abstract class RefluxServerEndpoint implements ApplicationContextAware {\n\n    private static final Logger log = LoggerFactory.getLogger(RefluxServerEndpoint.class);\n\n    private static RefluxServerImpl connectionManager;\n\n    public static final void setConnectionManager(RefluxServerImpl connectionManager) {\n        RefluxServerEndpoint.connectionManager = connectionManager;\n    }\n\n    private Session session;\n\n    private ClientConnectionInfo client = null;\n\n    private boolean sendable = false;\n\n    private ApplicationContext context = null;\n\n    public final ApplicationContext getApplicationContext() {\n        return context;\n    }\n\n    @Override\n    public final void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n        context = applicationContext;\n    }\n\n    public final void setSendable(boolean sendable) {\n        this.sendable = sendable;\n    }\n\n    private void close(Session session) {\n        String clientId = getClientId();\n        if (connectionManager != null) {\n            connectionManager.onClose(clientId);\n        }\n\n        if (session != null && session.isOpen()) {\n            try {\n                session.close();\n            } catch (IOException e) {\n                e.printStackTrace();\n            }\n        }\n\n        if (this.session != session && this.session != null && session.isOpen()) {\n            try {\n                this.session.close();\n            } catch (IOException e) {\n                e.printStackTrace();\n            }\n        }\n        this.session = null;\n        this.client = null;\n        this.sendable = false;\n    }\n\n    protected abstract boolean authenticate(String clientId);\n\n    protected String getClientId(Session session) {\n        List<String> clientIds = session.getRequestParameterMap().get(\"clientId\");\n        if (clientIds == null || clientIds.size() == 0) {\n            log.warn(\"来自客户端的连接, clientId is empty\");\n            return null;\n        }\n        if (clientIds.size() > 1) {\n            log.warn(\"来自客户端的连接, clientId more than one: {}\", clientIds);\n            return null;\n        }\n        String clientId = clientIds.get(0);\n        if (log.isInfoEnabled()) {\n            log.info(\"来自客户端的连接, clientId = {}\", clientId);\n        }\n        return clientId;\n    }\n\n    @OnOpen\n    public final void onOpen(Session session) throws BusinessException {\n        if (connectionManager == null) {\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n                    .setMessage(\"Server not inited...\");\n        }\n\n        this.session = session;\n        String clientId = getClientId(session);\n        if (!authenticate(clientId)) {\n            close(session);\n            return;\n        }\n        try {\n            client = connectionManager.onOpen(clientId, this);\n        } catch (BusinessException e) {\n            close(session);\n            return;\n        }\n\n    }\n\n    @OnClose\n    public final void onClose() {\n        if (log.isInfoEnabled()) {\n            log.info(\"关闭客户端连接: {}\", client);\n        }\n        close(session);\n    }\n\n    @OnMessage\n    public final void onMessage(String message, Session session) throws IOException {\n        if (log.isInfoEnabled()) {\n            log.info(\"来自收集端的消息: {}\", message);\n        }\n    }\n\n    @OnError\n    public final void onError(Session session, Throwable e) {\n        if (e instanceof EOFException) {\n            System.out.println(\"连接异常中断.\");\n            return;\n        }\n        log.error(\"Client Connection Error: {}\", e.getMessage(), e);\n    }\n\n    public final boolean isSendable() {\n        return sendable && client != null && session != null && session.isOpen();\n    }\n\n    public final void sendMessage(String message) throws IOException {\n        if (isSendable()) {\n            synchronized (this) {\n                Async async = session.getAsyncRemote();\n                async.sendText(message);\n                if (log.isInfoEnabled()) {\n                    log.info(\"sendMessage done\\nclient: {}\\nmessage: {}\", client, message);\n                }\n            }\n        } else {\n            if (log.isInfoEnabled()) {\n                log.info(\"Connection is not sendable, sendable = {}, client = {}, session.isOpen() = {}\",\n                        sendable, client, session.isOpen());\n            }\n        }\n    }\n\n    ////////////////////////////////////////////////////////////////\n\n    String getClientId() {\n        return client == null ? null : client.getClientId();\n    }\n\n}\n"
  },
  {
    "path": "commons-reflux/src/main/java/com/terran4j/commons/reflux/server/RefluxServerImpl.java",
    "content": "package com.terran4j.commons.reflux.server;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport javax.annotation.PostConstruct;\n\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Service;\nimport org.springframework.util.StringUtils;\n\nimport com.google.gson.Gson;\nimport com.terran4j.commons.reflux.Message;\nimport com.terran4j.commons.reflux.RefluxServer;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\n\n/**\n * 管理所有的 WebSocket 连接。\n * \n * @author jiangwei\n *\n */\n@Service\npublic class RefluxServerImpl implements RefluxServer {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(RefluxServerImpl.class);\n\t\n\tprivate static final Gson gson = new Gson();\n\n\t/**\n\t * clientId 与 Connection 的关系。\n\t */\n\tprivate static final Map<String, ClientConnectionInfo> connectionInfoes = new ConcurrentHashMap<>();\n\t\n\t@PostConstruct\n\tpublic void saveMe() {\n\t\tRefluxServerEndpoint.setConnectionManager(this);\n\t}\n\t\n\tpublic void onClose(String clientId) {\n\t\tif (StringUtils.isEmpty(clientId)) {\n\t\t\treturn;\n\t\t}\n\t\tclientId = clientId.trim();\n\t\t\n\t\tClientConnectionInfo connInfo = connectionInfoes.remove(clientId);\n\t\tif (connInfo == null) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"关闭一个连接，现在连接数为： {}\", getConnectionCount());\n\t\t}\n\t}\n\t\n\tpublic ClientConnectionInfo onOpen(String clientId, RefluxServerEndpoint conn) throws BusinessException {\n\t\tif (StringUtils.isEmpty(clientId)) {\n\t\t\tthrow new BusinessException(ErrorCodes.NULL_PARAM)\n\t\t\t\t\t.put(\"clientId\", clientId);\n\t\t}\n\t\t\n\t\tClientConnectionInfo connInfo = connectionInfoes.get(clientId);\n\t\tif (connInfo == null) { // 首次连接，保留连接信息。\n\t\t\tconnInfo = new ClientConnectionInfo();\n\t\t\tconnInfo.setClientId(clientId);\n\t\t\tconnInfo.setConnectedTime(System.currentTimeMillis());\n\t\t\tconnInfo.setConnection(conn);\n\t\t\tconnectionInfoes.put(clientId, connInfo);\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"有新连接加入! 当前连接数为 {}\", getConnectionCount());\n\t\t\t}\n\t\t} else { // 重新连接，只更新连接对象就可以了。\n\t\t\tconnectionInfoes.get(clientId).setConnection(conn);\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"重新建立连接! 当前连接数为 {}\", getConnectionCount());\n\t\t\t}\n\t\t}\n\t\t\n\t\t// 开启推送开关。\n\t\tconn.setSendable(true);\n\t\t\n\t\treturn connInfo;\n\t}\n\t\n\tpublic List<RefluxServerEndpoint> getConnections() {\n\t\tList<RefluxServerEndpoint> conns = new ArrayList<>();\n\t\t\n\t\tIterator<String> it = connectionInfoes.keySet().iterator();\n\t\twhile (it.hasNext()) {\n\t\t\tString clientId = it.next();\n\t\t\tClientConnectionInfo info = connectionInfoes.get(clientId);\n\t\t\tif (info == null) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t\n\t\t\tRefluxServerEndpoint conn = info.getConnection();\n\t\t\tif (conn == null) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t\n\t\t\tconns.add(conn);\n\t\t}\n\t\treturn conns;\n\t}\n\t\n\tpublic RefluxServerEndpoint getConnection(String clientId) {\n\t\tClientConnectionInfo info = connectionInfoes.get(clientId);\n\t\tif (info == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn info.getConnection();\n\t}\n\n\tpublic int getConnectionCount() {\n\t\treturn connectionInfoes.size();\n\t}\n\n\tpublic static interface ConnectionHandler {\n\t\tvoid exe(RefluxServerEndpoint conn);\n\t}\n\n\tpublic void dispatch(ConnectionHandler handler) {\n\t\tif (getConnectionCount() <= 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tIterator<String> it = connectionInfoes.keySet().iterator();\n\t\twhile (it.hasNext()) {\n\t\t\tString clientId = it.next();\n\t\t\tRefluxServerEndpoint conn = connectionInfoes.get(clientId).getConnection();\n\t\t\thandler.exe(conn);\n\t\t}\n\t}\n\t\n\t@Override\n\tpublic final boolean isConnected(String clientId) {\n\t\treturn connectionInfoes.containsKey(clientId);\n\t}\n\t\n\t@Override\n\tpublic final <T> boolean send(T content, String clientId) {\n\t\tif (content == null) {\n\t\t\tthrow new NullPointerException(\"message is null.\");\n\t\t}\n\t\tif (clientId == null) {\n\t\t\tthrow new NullPointerException(\"clientId is null.\");\n\t\t}\n\t\t\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"start to send message to client【{}】, message:\\n{}\", // \n\t\t\t\t\tclientId, Strings.toString(content));\n\t\t}\n\t\t\n\t\tClientConnectionInfo info = connectionInfoes.get(clientId);\n\t\tif (info == null) {\n\t\t\tif (log.isWarnEnabled()) {\n\t\t\t    log.warn(\"Can't get Client Connection by clientId: {}\", clientId);\n\t\t\t}\t\t\t\n\t\t\treturn false;\n\t\t}\n\t\treturn sendContent(content, info);\n\t}\n\t\n\n\t@Override\n\tpublic final <T> int sendAll(T content) {\n\t\tif (content == null) {\n\t\t\tthrow new NullPointerException(\"message is null.\");\n\t\t}\n\t\t\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"start to send message: {}\", Strings.toString(content));\n\t\t}\n\t\tif (connectionInfoes == null || connectionInfoes.size() == 0) {\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"NO ANY connection, message won't be sent.\");\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\t\t\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"{} connections from clients\", connectionInfoes.size());\n\t\t}\n\t\tint successCount = 0;\n\t\tCollection<ClientConnectionInfo> conns = connectionInfoes.values();\n\t\tfor (ClientConnectionInfo info : conns) {\n\t\t\tif (sendContent(content, info)){\n\t\t\t\tsuccessCount++;\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn successCount;\n\t}\n\t\n\tfinal <T> boolean sendContent(T content, ClientConnectionInfo info) {\n\t\tif (info == null) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\tRefluxServerEndpoint conn = info.getConnection();\n\t\tif (conn == null) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\tMessage message = new Message();\n\t\tmessage.setId(Message.generateId());\n\t\tmessage.setStatus(Message.STATUS_REQUEST);\n\t\tString type = Classes.getTargetClass(content).getName();\n\t\tmessage.setType(type);\n\t\tmessage.setContent(content);\n\t\tString messageText = gson.toJson(message);\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"send message to client: {}\", conn.getClientId());\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"sending message: {}\", messageText);\n\t\t\t}\n\t\t\tconn.sendMessage(messageText);\n\t\t\treturn true;\n\t\t} catch (IOException e) {\n\t\t\te.printStackTrace();\n\t\t\t// TODO: 处理异常。\n\t\t}\n\t\t\n\t\treturn false;\n\t}\n\n}\n"
  },
  {
    "path": "commons-reflux/src/test/java/com/terran4j/test/commons/reflux/Hello.java",
    "content": "package com.terran4j.test.commons.reflux;\n\npublic class Hello {\n\n\tprivate String name;\n\t\n\tprivate String greeting;\n\t\n\tprivate long currentTime;\n\t\n\tpublic Hello() {\n\t\tsuper();\n\t}\n\t\n\tpublic Hello(String name) {\n\t\tthis(name, \"Hello, \" + name);\n\t}\n\n\tpublic Hello(String name, String greeting) {\n\t\tsuper();\n\t\tthis.name = name;\n\t\tthis.greeting = greeting;\n\t\tthis.currentTime = System.currentTimeMillis();\n\t}\n\n\t/**\n\t * @return the name\n\t */\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\n\t/**\n\t * @param name the name to set\n\t */\n\tpublic final void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\t/**\n\t * @return the greeting\n\t */\n\tpublic final String getGreeting() {\n\t\treturn greeting;\n\t}\n\n\t/**\n\t * @param greeting the greeting to set\n\t */\n\tpublic final void setGreeting(String greeting) {\n\t\tthis.greeting = greeting;\n\t}\n\n\t/**\n\t * @return the currentTime\n\t */\n\tpublic final long getCurrentTime() {\n\t\treturn currentTime;\n\t}\n\n\t/**\n\t * @param currentTime the currentTime to set\n\t */\n\tpublic final void setCurrentTime(long currentTime) {\n\t\tthis.currentTime = currentTime;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-reflux/src/test/java/com/terran4j/test/commons/reflux/RefluxApplication.java",
    "content": "package com.terran4j.test.commons.reflux;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.reflux.EnableReflux;\n\n@EnableReflux\n@SpringBootApplication\npublic class RefluxApplication {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(RefluxApplication.class, args);\n\t}\n\t\n}"
  },
  {
    "path": "commons-reflux/src/test/java/com/terran4j/test/commons/reflux/SendAndReceiveTest.java",
    "content": "package com.terran4j.test.commons.reflux;\n\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\n\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.test.context.SpringBootTest.WebEnvironment;\nimport org.springframework.test.context.TestExecutionListeners;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.test.context.support.DependencyInjectionTestExecutionListener;\n\nimport com.terran4j.commons.reflux.OnMessage;\nimport com.terran4j.commons.reflux.RefluxClient;\nimport com.terran4j.commons.reflux.RefluxServer;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.error.BusinessException;\n\n@SpringBootTest(classes = { RefluxApplication.class }, webEnvironment = WebEnvironment.DEFINED_PORT)\n@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class })\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class SendAndReceiveTest {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(SendAndReceiveTest.class);\n\n\tprivate CountDownLatch flag;\n\n\t@Autowired\n\tprivate RefluxClient refluxClient;\n\t\n\t@Autowired\n\tprivate RefluxServer refluxServer;\n\t\n\t@Value(\"${server.wsURL:ws://localhost:8080/websocket/connect}\")\n\tprivate String serverURL;\n\t\n\tprivate String clientId = null;\n\t\n\t@Before\n\tpublic void setUp() throws BusinessException {\n\t\tclientId = TestServerEndpoint.generateClientId();\n\t\tboolean connected = refluxClient.connect(serverURL, clientId);\n\t\tAssert.assertTrue(connected);\n\t}\n\n\t@OnMessage\n\tpublic void onHello(Hello hello) {\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"receive message, hello = {}\", Strings.toString(hello));\n\t\t}\n\t\tflag.countDown();\n\t}\n\n\t@Test\n\tpublic void testSendAndReceive() throws InterruptedException {\n\t\tflag = new CountDownLatch(1);\n\t\tHello hello = new Hello(\"terran4j\");\n\t\tlong t0 = System.currentTimeMillis();\n\t\trefluxServer.send(hello, clientId);\n\t\ttry {\n\t\t\tboolean success = flag.await(2000, TimeUnit.MILLISECONDS);\n\t\t\tAssert.assertTrue(success);\n\t\t} catch (InterruptedException e) {\n\t\t\tAssert.fail(\"InterruptedException: \" + e.getMessage());\n\t\t}\n\t\tlong t = System.currentTimeMillis() - t0;\n\t\tlog.info(\"Send and Receive, spend: {}ms\", t);\n\t}\n\n}\n"
  },
  {
    "path": "commons-reflux/src/test/java/com/terran4j/test/commons/reflux/TestServerEndpoint.java",
    "content": "package com.terran4j.test.commons.reflux;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport javax.websocket.server.ServerEndpoint;\n\nimport org.springframework.stereotype.Component;\n\nimport com.terran4j.commons.reflux.server.RefluxServerEndpoint;\n\n@ServerEndpoint(\"/websocket/connect\")\n@Component\npublic class TestServerEndpoint extends RefluxServerEndpoint {\n\t\n\tpublic static final Set<String> clientIds = new HashSet<>();\n\n\tpublic static final String generateClientId() {\n\t\tString clientId = UUID.randomUUID().toString();\n\t\tclientIds.add(clientId);\n\t\treturn clientId;\n\t}\n\t\n\t@Override\n\tprotected boolean authenticate(String clientId) {\n\t\treturn clientIds.contains(clientId);\n\t}\n\n}\n"
  },
  {
    "path": "commons-restpack/.gitignore",
    "content": "/target/\n/.classpath\n/.project\n/.settings/\n/restpack.log\nterran4j-commons-restpack.iml\n"
  },
  {
    "path": "commons-restpack/README.md",
    "content": "\n\n## 目录\n\n* 项目背景\n* RestPack 简介\n* 引入 RestPack 依赖\n* 启用 RestPack\n* @RestPackController 注解\n* RestPack 异常处理\n* 自定义数据格式\n* 日志输出\n\n\n## 项目背景\n\n在互联网、移动互联网、车联网、物联网繁荣的今天，\n各种客户端设备层出不穷，为了能用同一套服务端程序处理各种客户端的访问，\n[ HTTP Restful API ](http://www.ruanyifeng.com/blog/2014/05/restful_api.html) \n变得流行起来。\n\n但是客户端与服务端交互时，往往会有一些通用的需求，比如：\n* 服务端返回的报文，有一套统一的标准，这样有利于开发和维护。\n* 服务端在处理一个 API 请求时，如果出异常了，\n     总是希望在请求的返回结果中给出一个明确的错误码，\n     客户端可以根据错误码作进一步的处理。\n* 为了方便排查问题，总是希望对于每个请求，服务端会返回一个 requestId，\n    后台可以将这个请求产生的日志与这个 requestId 相关联。\n    这样一旦前后端联调时发现了问题，前端工程师只要给出 requestId ，\n    后台工程师就可以拿着这个 requestId 快速找出相关日志，方便分析排查问题。\n......\n\n为了满足这些非功能性需求，笔者总结了之前很多项目的开发经验，\n归纳出一套统一的数据返回格式，如下（分成功和失败两种情况）：\n\n成功响应内容：\n```json\n{\n  \"requestId\" : \"d56c24d006aa4d5e9b8903b3256bf3e3\",\n  \"serverTime\" : 1502592752449,\n  \"spendTime\" : 5,\n  \"resultCode\" : \"success\",\n  \"data\" : {\n    \"key1\": \"value1\",\n    \"key2\": \"value2\"\n  }\n}\n```\n* requestId ： 服务端生成的请求唯一ID号，\n    当这个请求有问题时，可以拿着这个 ID 号，\n    在海量日志快速查询到此请求的日志信息，以方便排查问题。\n* serverTime ： 服务器时间，\n    很多场景下需要使用当前时间值，但客户端本地的时间有可能不准，\n    因为这里返回服务器端时间供客户端使用。\n* spendTime ： 本次请求在服务器端处理所消耗的时间，\n    这里显示出来以方便诊断慢请求相关问题。\n* resultCode ： 结果码，\n    \"success\" 表示成功，其它表示一个错误的错误码，\n    错误码的值及具体含意由项目中客户端与服务端约定。 \n* data :  实际的业务数据，内容由每个 API 的业务逻辑决定。\n\n错误响应内容：\n```json\n{\n  \"requestId\" : \"d7ab68ac513e4549896aa33f0cda3518\",\n  \"serverTime\" : 1502594589673,\n  \"spendTime\" : 8,\n  \"resultCode\" : \"name.duplicate\",\n  \"message\" : \"昵称重复： terran4j，请换个昵称！\",\n  \"props\" : {\n    \"name\": \"terran4j\"\n  }\n}\n```\n与成功响应类似，都有 requestId、serverTime、spendTime 等字段。\n不同的是 resultCode 是一个自定义的错误码，并且多了message 、props 两个字段：\n* message ： 错误信息描述，\n    是一段易于人理解的字符串信息，方便开发人员知晓错误原因。\n* props ： 错误上下文相关属性，\n    本项可选，有的错误码可能需要前端在程序中作进一步处理，\n    所以后台可以在 props 中提供一些 key - value 的属性值，\n    方便程序读取（而不是让前端程序从 message 中解析文本内容获取这些值）。\n\n\n## RestPack 简介\n\n若要让项目中每个 API 的实现都遵循这套统一的数据规范，\n无疑要在每个API方法中编写一些重复性的代码。\n因此笔者根据实际项目经验总结，开发了一套名为 **RestPack** 的工具包，\n可以帮助 Restful API 的开发者将API 的返回结果自动包装成统一格式的报文。\n\nRestPack 一词中， Rest 代表 Http Restful API 的意思，\n而 Pack 是 \"包装、包裹\" 的意思，合起来的意思就是在原本的 Http Restful API 基础上，\n将返回数据再包裹一层，以符合之前所讲的数据规范。\n\n本文主要目标是介绍 RestPack 的用法。\n\n\n## 引入 RestPack 依赖\n\n然后，您就可以在您的项目的 pom.xml 文件中，引用 restpack 的 jar 包了，如下所示：\n```\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-restpack</artifactId>\n\t\t\t<version>${restpack.version}</version>\n\t\t</dependency>\n```\n\n整个 pom.xml 内容类似于：\n\n```xml\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<groupId>terran4j</groupId>\n\t<artifactId>terran4j-demo-restpack</artifactId>\n\t<version>0.0.1-SNAPSHOT</version>\n\t<packaging>jar</packaging>\n\n\t<name>terran4j-demo-restpack</name>\n\n\t<parent>\n\t\t<groupId>org.springframework.boot</groupId>\n\t\t<artifactId>spring-boot-starter-parent</artifactId>\n\t\t<version>1.5.9.RELEASE</version>\n\t</parent>\n\n\t<properties>\n\t\t<java.version>1.8</java.version>\n\t</properties>\n\n\t<dependencies>\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-web</artifactId>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>terran4j</groupId>\n\t\t\t<artifactId>terran4j-commons-restpack</artifactId>\n\t\t\t<version>${restpack.version}</version>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n```\n\n如果是 gradle，请在 build.gradle 中添加依赖，如下所示：\n\n```groovy\ncompile \"com.github.terran4j:terran4j-commons-restpack:${restpack.version}\"\n```\n\n${restpack.version} **最新稳定版，请参考 [这里](https://github.com/terran4j/commons/blob/master/version.md)**\n\n\n## 启用 RestPack \n\n为了在应用程序中启用 RestPack，需要在 SpringBootApplication 类上加`@EnableRestPack` 注解，\n整个 main 程序代码，如下所示：\n\n```java\npackage com.terran4j.demo.restpack;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport com.terran4j.commons.restpack.EnableRestPack;\n\n@EnableRestPack\n@SpringBootApplication\npublic class RestPackDemoApp {\n\n\tpublic static void main(String[] args) {\n\t\tSpringApplication.run(RestPackDemoApp.class, args);\n\t}\n\n}\n```\n**加上 @EnableRestPack 才能启用 RestPack 的功能，否则本文下面所讲的效果都不会起作用。**\n\n\n## @RestPackController 注解\n\n以前实现 HTTP Restful API，就是用 Spring Boot MVC 编写一个 Controller 类，\n并在类上加上 @RestController 注解\n（对于这一点不清楚的读者，请先阅读笔者之前写过的\n《[ Spring Boot快速入门 ](http://www.jianshu.com/nb/14688855?order_by=seq)》\n一书，其中《[ Spring Boot MVC ](http://www.jianshu.com/p/e2d44f38287e)》\n这章详细描述了这一点）。\n\n要在原有的 Controller 类上启用 RestPack 功能，\n仅仅是将类上的注解由 @RestController 改成 @RestPackController 就可以了，\n代码如下所示：\n\n```java\npackage com.terran4j.demo.restpack;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RequestParam;\n\nimport com.terran4j.commons.restpack.HttpResultPackController;\nimport com.terran4j.commons.util.error.BusinessException;\n\n@RestPackController\n@RequestMapping(\"/demo/restpack\")\npublic class RestPackDemoController {\n    \n    private static final Logger log = LoggerFactory.getLogger(RestPackDemoController.class);\n\n\t@RequestMapping(value = \"/echo\", method = RequestMethod.GET)\n\tpublic String echo(@RequestParam(value = \"msg\") String msg) throws BusinessException {\n\t    log.info(\"echo, msg = {}\", msg);\n\t\treturn msg;\n\t}\n\t\n}\n```\n\n编写好这个类后，我们启动 main 程序，然后浏览器输入URL：\n\n```\nhttp://localhost:8080/demo/restpack/echo?msg=abc\n```\n浏览器中显示结果为：\n```json\n{\n  \"requestId\" : \"2141d927f1de453ba3edd83306ecdf3e\",\n  \"serverTime\" : 1502597485688,\n  \"spendTime\" : 21,\n  \"resultCode\" : \"success\",\n  \"data\" : \"abc\"\n}\n```\n\n如果我们去掉 @EnableRestPack （或将 @RestPackController 还原成 @RestController），\n再访问的结果仅为：\n\n```\nabc\n```\n说明 RestPack 可以将原本的返回数据，自动包装成我们定义的数据规范格式了。\n\n对于无返回值的方法， RestPack 同样有效果，\n比如我们在上面的 RestPackDemoController 类中添加如下方法：\n\n```java\n@RestPackController\n@RequestMapping(\"/demo/restpack\")\npublic class RestPackDemoController {\n    \n    @RequestMapping(value = \"/void\", method = RequestMethod.GET)\n    public void doVoid(@RequestParam(value = \"msg\") String msg) throws BusinessException {\n        log.info(\"doVoid, msg = {}\", msg);\n    }\n}\n```\n\n重启程序后在浏览器输入URL:\n```\nhttp://localhost:8080/demo/restpack/void?msg=abc\n```\n显示的结果如下:\n```json\n{\n  \"requestId\" : \"2df4aa14dfab46e196ebf7e79b2b35d6\",\n  \"serverTime\" : 1502627058784,\n  \"spendTime\" : 35,\n  \"resultCode\" : \"success\"\n}\n```\n\n由于方法没有返回值，所以\"data\"字段也不出现了，但其它字段都有了。\n\n如果返回值是自定义的复杂对象，RestPack 同样能转化成 json 格式放在 \"data\" 字段中，\n比如我们再添加如下代码：\n\n```java\n\n@RestPackController\n@RequestMapping(\"/demo/restpack\")\npublic class RestPackDemoController {\n    \n    @RequestMapping(value = \"/hello\", method = RequestMethod.GET)\n    public HelloBean hello(@RequestParam(value = \"name\") String name) throws BusinessException {\n        log.info(\"hello, name = {}\", name);\n        HelloBean bean = new HelloBean();\n        bean.setName(name);\n        bean.setMessage(\"Hello, \" + name + \"!\");\n        bean.setTime(new Date());\n        return bean;\n    }\n}\n```\n\n类 HelloBean 的定义如下：\n\n```java\npackage com.terran4j.demo.restpack;\n\nimport java.util.Date;\n\npublic class HelloBean {\n\t\n\tprivate String name;\n\t\n\tprivate String message;\n\t\n\tprivate Date time;\n\n    // 省略 getter /setter 方法。\n\t\n}\n\n```\n重启程序后在浏览器输入URL:\n```\nhttp://localhost:8080/demo/restpack/hello?name=neo\n```\n\n显示的结果如下:\n\n```json\n{\n  \"requestId\" : \"ab5c43c3415042b682b290e17fad1358\",\n  \"serverTime\" : 1502957833154,\n  \"spendTime\" : 30,\n  \"resultCode\" : \"success\",\n  \"data\" : {\n    \"name\" : \"neo\",\n    \"message\" : \"Hello, neo!\",\n    \"time\" : \"2017-08-17 16:17:13\"\n  }\n}\n```\n\n发现 \"data\" 中的字段与 HelloBean 的属性是对应的。\n\n\n## RestPack 异常处理\n\n当服务端抛出异常时，RestPack 会将异常包装成错误报文返回。\n\n从客户端的角度来看，异常分两种：\n* 一种是业务异常，\n    如： 注册时用户名已存在、用户输入错误，等。这种情况下，\n    客户端需要明确的异常原因及关键字段数据，\n    以便于客户端程序知晓如何在界面上给予用户提示。\n* 另一种是系统异常，\n    如： 数据库无法访问、程序BUG，等。\n    这种异常需要客户端模糊处理（尽量避免暴露系统本身的问题），\n    比如弹出一个“对不起，系统开小差了”，\n    或“系统维护中，请稍后重试”之类的提示。\n\nRestPack 提供了一个叫 BusinessException 的异常类来代表业务异常，\n如果方法抛出的异常类是 BusinessException 类或其子类，\nRestPack 就按业务异常处理，如果不是就按系统异常处理。\n为了查看运行效果，我们添加一个新的方法：\n\n```java\n@RestPackController\n@RequestMapping(\"/demo/restpack\")\npublic class RestPackDemoController {\n    \n    @RequestMapping(value = \"/regist\", method = RequestMethod.GET)\n    public void regist(@RequestParam(value = \"name\") String name) throws BusinessException {\n        log.info(\"regist, name = {}\", name);\n        if (name.length() < 3) {\n            String suggestName = name + \"123\";\n            throw new BusinessException(\"name.invalid\")\n                    .setMessage(\"您输入的名称太短了，建议为：${suggestName}\")\n                    .put(\"suggestName\", suggestName);\n        }\n        log.info(\"regist done, name = {}\", name);\n    }\n}\n```\n\n在 BusinessException 类中，构造方法中的参数(如上面的 \"name.invalid\" ) 就是错误码，\n `put(String, Object)`   方法用于设置一些异常上下文属性，会出现在返回报文的 props 字段中，\n`setMessage(String)`  方法用于设置异常信息，可以用 `${}` 来引用 `put` 方法出现的字段。\n\n重启程序，在浏览器中访问URL:\n```\nhttp://localhost:8080/demo/restpack/regist?name=ne\n```\n结果如下：\n```json\n{\n  \"requestId\" : \"22e5651199f645628fdf724e9f0826a3\",\n  \"serverTime\" : 1502627761012,\n  \"spendTime\" : 1,\n  \"resultCode\" : \"name.invalid\",\n  \"message\" : \"您输入的名称太短了，建议为：ne123\",\n  \"props\" : {\n    \"suggestName\" : \"ne123\"\n  }\n}\n```\n\n## 自定义数据格式\n\n或许你的项目需要自定义返回报文的数据格式，而不是使用 RestPack 默认的这一套数据格式，\n可以有两种做法：\n\n第一，对这些通用字段进行重命名，在 application.yml 配置文件中定义如下：\n\n```yaml\nterran4j:\n  restpack:\n    renaming:\n      requestId: requestCode\n      serverTime: currentTime\n      spendTime: spend\n      resultCode: status\n      data: result\n      message: msg\n      props: data\n      success: OK\n```\nterran4j.restpack.renaming 下的配置项是对相应的字符进行重命名，\n如： requestId 被改成 requestCode，serverTime 被改成 serverTime......\n其中最后一个 `success: OK` 表示对请求成功时的返回码重命名为\"OK\"(默认为\"success\")。\n\n重命名后数据的组织结构没有变，只是字段名改了，如下所示：\n\n```json\n{\n  \"requestCode\" : \"22e5651199f645628fdf724e9f0826a3\",\n  \"currentTime\" : 1502627761012,\n  \"spend\" : 1,\n  \"status\" : \"OK\",\n  \"result\" : {\n    \"name\" : \"neo\",\n    \"message\" : \"Hello, neo!\",\n    \"time\" : \"2017-08-17 16:17:13\"\n  }\n}\n```\n \n如果你希望连数据结构也自定义，则需要使用第二种方式了。\n\n第二种方式：编写一个服务实现 HttpResultConverter 接口，并注册到 Spring 容器中，\n如下代码所示：\n\n```java\n\npackage com.terran4j.demo.restpack;\n\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.restpack.HttpResultConverter;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class DemoHttpResultConverter implements HttpResultConverter {\n\n    @Override\n    public Object convert(HttpResult httpResult) {\n        // 这里可以将 HttpResult 对象转成你需要的格式\n        // RestPack 框架会将本方法的返回对象转成 JSON 串返回给请求方。\n        return httpResult;\n    }\n}\n\n```\n\n注意，整个应用程序不可以注册多个 HttpResultConverter 接口的实现对象，\n否则 RestPack 不知道使用哪个就会在启动时报错。\n\n\n## 日志输出\n\nRestPack 在开始处理请求时，会生成唯一的 requestId，\n这个 requestId 不但会在返回报文中出现，还会一开始就放到日志的**MDC**中，\n对于 log4j 或 logback （它们都支持 MDC），\n你可以在配置将 requestId 信息输出到日志中，这样每条日志就用 requestId 相关联了。\n\n比如在项目中，将 logback.xml 配置如下：\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration debug=\"false\" scan=\"true\" scanPeriod=\"1000\">\n\n\t<appender name=\"stdout\" class=\"ch.qos.logback.core.ConsoleAppender\">\n\t\t<encoder>\n\t\t\t<pattern>%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n</pattern>\n\t\t</encoder>\n\t</appender>\n\n\t<appender name=\"file\" class=\"ch.qos.logback.core.FileAppender\">\n\t\t<file>./restpack.log</file>\n\t\t<encoder>\n\t\t\t<pattern>%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n</pattern>\n\t\t</encoder>\n\t</appender>\n\n\t<root level=\"info\">\n\t\t<appender-ref ref=\"stdout\" />\n\t\t<appender-ref ref=\"file\" />\n\t</root>\n\n</configuration>\n\n```\n\n**重点是日志输出格式，也就是<pattern>中加上requestId=%X{requestId}**：\n\n```\n%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n\n```\n%X{} 是使用 MDC 中的字段，有关 logback / log4j 中 MDC 的用法，不清楚的读者请自行百度搜索。\n\nlogback.xml 配置好后，再重启服务，在浏览器中输入URL：\n```\nhttp://localhost:8080/demo/restpack/echo?msg=abc\n```\n结果控制台输出如下：\n```\n2017-08-17 16:34:08,570 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.commons.restpack.RestPackAspect     [120]: request '/demo/restpack/echo' begin, params:\n{\n  \"msg\" : \"abc\"\n}\n2017-08-17 16:34:08,571 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.d.r.RestPackDemoController          [29]: echo, msg = abc\n2017-08-17 16:34:08,572 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.commons.restpack.RestPackAdvice     [63]: request '/demo/restpack/echo' end, response:\n{\n  \"requestId\" : \"ca2a12a0031f493db97856a3300b917a\",\n  \"serverTime\" : 1502958848570,\n  \"spendTime\" : 2,\n  \"resultCode\" : \"success\",\n  \"data\" : \"abc\"\n}\n```\n可以看到日志中有`requestId=ca2a12a0031f493db97856a3300b917a` 这段内容。\n\n这样的好处是排查日志方便，比如在 linux 环境中，对日志文件执行类似如下命令：\n```\ngrep -n \"requestId=ca2a12a0031f493db97856a3300b917a\" xxx.log\n```\n(xxx.log 是程序产生的日志文件的名称)，\n就可以在大量日志内容中快速过滤出这条请求的日志了。\n"
  },
  {
    "path": "commons-restpack/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.github.terran4j</groupId>\n        <artifactId>terran4j-commons-parent</artifactId>\n        <version>1.0.4-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>terran4j-commons-restpack</artifactId>\n    <packaging>jar</packaging>\n    <name>terran4j-commons-restpack</name>\n    <url>https://github.com/terran4j/commons</url>\n\n    <dependencies>\n\n        <!-- terran4j 工具类库。 -->\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-website</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-aop</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.aspectj</groupId>\n            <artifactId>aspectjweaver</artifactId>\n        </dependency>\n\n        <!-- json -->\n        <dependency>\n            <groupId>com.google.code.gson</groupId>\n            <artifactId>gson</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n\n        <!-- FreeMarker支持 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-freemarker</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n        </dependency>\n\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/EnableRestPack.java",
    "content": "package com.terran4j.commons.restpack;\n\nimport com.terran4j.commons.restpack.config.RestPackConfiguration;\nimport com.terran4j.commons.website.config.WebsiteConfiguration;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.web.servlet.config.annotation.EnableWebMvc;\n\nimport java.lang.annotation.*;\n\n/**\n * 启用 RestPack 服务。\n *\n * @author jiangwei\n */\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@EnableWebMvc\n@Import({\n        RestPackConfiguration.class,\n        WebsiteConfiguration.class\n})\npublic @interface EnableRestPack {\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/HttpResult.java",
    "content": "package com.terran4j.commons.restpack;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.terran4j.commons.restpack.impl.RestPackAspect;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCode;\nimport org.springframework.util.StringUtils;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic class HttpResult {\n\n    public static final String SUCCESS_CODE = \"success\";\n\n    private String requestId;\n\n    /**\n     * 服务端开始处理的时间。\n     */\n    private long serverTime;\n\n    private long spendTime;\n\n    private String resultCode;\n\n    private Object data;\n\n    private String message;\n\n    private Map<String, Object> props = null;\n\n    private List<String> logs = null;\n\n    public static HttpResult success() {\n        HttpResult response = new HttpResult();\n        response.setResultCode(SUCCESS_CODE);\n        return response;\n    }\n\n    public static HttpResult success(Object data) {\n        HttpResult response = new HttpResult();\n        response.setResultCode(SUCCESS_CODE);\n        response.setData(data);\n        return response;\n    }\n\n    private static final long defaultServerTime = System.currentTimeMillis();\n\n    private static final String defaultRequestId = RestPackAspect.generateRequestId();\n\n    public static HttpResult successFully(Object data) {\n        HttpResult response = new HttpResult();\n        response.setRequestId(defaultRequestId);\n        response.setSpendTime(5);\n        response.setServerTime(defaultServerTime);\n        response.setResultCode(SUCCESS_CODE);\n        response.setData(data);\n        return response;\n    }\n\n    public static HttpResult fail(ErrorCode errorCode, String msg) {\n        if (errorCode == null) {\n            throw new NullPointerException(\"errorCode is null.\");\n        }\n        if (StringUtils.isEmpty(msg)) {\n            msg = errorCode.getName().replace('.', ' ');\n        }\n        HttpResult response = new HttpResult();\n        response.setResultCode(errorCode.getName());\n        response.setMessage(msg);\n        return response;\n    }\n\n    public static HttpResult fail(BusinessException e) {\n        HttpResult response = new HttpResult();\n        response.setResultCode(e.getErrorCode().getName());\n        response.setMessage(e.getMessage());\n\n        Map<String, Object> errProps = e.getProps();\n        if (errProps != null && errProps.size() > 0) {\n            if (response.props == null) {\n                response.props = new HashMap<>();\n            }\n            response.props.putAll(errProps);\n        }\n\n        return response;\n    }\n\n    public void clearPropsIfEmpty() {\n        if (props != null && props.size() == 0) {\n            props = null;\n        }\n    }\n\n    public String getRequestId() {\n        return requestId;\n    }\n\n    public void setRequestId(String requestId) {\n        this.requestId = requestId;\n    }\n\n    public long getServerTime() {\n        return serverTime;\n    }\n\n    public void setServerTime(long serverTime) {\n        this.serverTime = serverTime;\n    }\n\n    public String getResultCode() {\n        return resultCode;\n    }\n\n    public void setResultCode(String resultCode) {\n        this.resultCode = resultCode;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n    public Object getData() {\n        return data;\n    }\n\n    public void setData(Object data) {\n        this.data = data;\n    }\n\n    public long getSpendTime() {\n        return spendTime;\n    }\n\n    public void setSpendTime(long spendTime) {\n        this.spendTime = spendTime;\n    }\n\n    public Map<String, Object> getProps() {\n        return props;\n    }\n\n    public void setProps(Map<String, Object> props) {\n        this.props = props;\n    }\n\n    public final String toString() {\n        return Strings.toString(this);\n    }\n\n    public List<String> getLogs() {\n        return logs;\n    }\n\n    public void setLogs(List<String> logs) {\n        this.logs = logs;\n    }\n}"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/HttpResultConverter.java",
    "content": "package com.terran4j.commons.restpack;\n\npublic interface HttpResultConverter {\n\n    Object convert(HttpResult httpResult);\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/Log.java",
    "content": "package com.terran4j.commons.restpack;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.METHOD;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n/**\n * 自动输出日志信息。\n *\n * @author wei.jiang\n */\n@Documented\n@Retention(RUNTIME)\n@Target(METHOD)\npublic @interface Log {\n\n    String value() default \"\";\n\n}"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/LogItem.java",
    "content": "package com.terran4j.commons.restpack;\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.google.gson.Gson;\nimport org.apache.commons.beanutils.BeanUtils;\nimport org.slf4j.MDC;\nimport org.springframework.util.StringUtils;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.util.Arrays;\nimport java.util.Map;\n\npublic class LogItem {\n\n    private static final Gson GSON = new Gson();\n\n    public static final String toJson(LogItem obj) {\n        return GSON.toJson(obj);\n    }\n\n    public static final LogItem fromJson(String json) {\n        return GSON.fromJson(json, LogItem.class);\n    }\n\n    public static final LogItem fromEvent(ILoggingEvent event) {\n        if (event == null) {\n            throw new NullPointerException(\"ILoggingEvent event is null.\");\n        }\n        LogItem item = new LogItem();\n\n        String loggerName = event.getLoggerName();\n        item.setLoggerName(loggerName);\n\n        String message = event.getMessage();\n        item.setMessage(message);\n\n        Object[] argObjects = event.getArgumentArray();\n        if (argObjects != null) {\n            final int length = argObjects.length;\n            String[] args = new String[length];\n            for (int i = 0; i < length; i++) {\n                Object argObject = argObjects[i];\n                args[i] = String.valueOf(argObject);\n            }\n            item.setArgs(args);\n        }\n\n        int levelInt = event.getLevel().levelInt;\n        item.setLevelInt(levelInt);\n\n        String threadName = event.getThreadName();\n        item.setThreadName(threadName);\n\n        long timeStamp = event.getTimeStamp();\n        item.setTimeStamp(timeStamp);\n\n        if (event.hasCallerData()) {\n            StackTraceElement[] callerData = event.getCallerData();\n            if (callerData != null && callerData.length > 0) {\n                StackTraceElement caller = callerData[0];\n                if (caller != null) {\n                    item.setDeclaringClass(caller.getClassName());\n                    item.setFileName(caller.getFileName());\n                    item.setMethodName(caller.getMethodName());\n                    item.setLineNumber(caller.getLineNumber());\n                }\n            }\n        }\n\n        Map<String, String> mdc = MDC.getCopyOfContextMap();\n        item.setMdc(mdc);\n\n        return item;\n    }\n\n    private String loggerName;\n\n    private String message;\n\n    private String[] args;\n\n    private int levelInt;\n\n    private String threadName;\n\n    private long timeStamp;\n\n    private String serverName;\n\n    private String serverIP;\n\n    private int serverPort;\n\n    private String serviceName;\n\n    private String declaringClass;\n\n    private String methodName;\n\n    private String fileName;\n\n    private int lineNumber;\n\n    private Map<String, String> mdc;\n\n    public String getLoggerName() {\n        return loggerName;\n    }\n\n    public void setLoggerName(String loggerName) {\n        this.loggerName = loggerName;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n    public String[] getArgs() {\n        return args;\n    }\n\n    public void setArgs(String[] args) {\n        this.args = args;\n    }\n\n    public int getLevelInt() {\n        return levelInt;\n    }\n\n    public void setLevelInt(int levelInt) {\n        this.levelInt = levelInt;\n    }\n\n    public String getThreadName() {\n        return threadName;\n    }\n\n    public void setThreadName(String threadName) {\n        this.threadName = threadName;\n    }\n\n    public long getTimeStamp() {\n        return timeStamp;\n    }\n\n    public void setTimeStamp(long timeStamp) {\n        this.timeStamp = timeStamp;\n    }\n\n    public String getServerName() {\n        return serverName;\n    }\n\n    public void setServerName(String serverName) {\n        this.serverName = serverName;\n    }\n\n    public String getServerIP() {\n        return serverIP;\n    }\n\n    public void setServerIP(String serverIP) {\n        this.serverIP = serverIP;\n    }\n\n    public int getServerPort() {\n        return serverPort;\n    }\n\n    public void setServerPort(int serverPort) {\n        this.serverPort = serverPort;\n    }\n\n    public String getServiceName() {\n        return serviceName;\n    }\n\n    public void setServiceName(String serviceName) {\n        this.serviceName = serviceName;\n    }\n\n    public String getDeclaringClass() {\n        return declaringClass;\n    }\n\n    public void setDeclaringClass(String declaringClass) {\n        this.declaringClass = declaringClass;\n    }\n\n    public String getMethodName() {\n        return methodName;\n    }\n\n    public void setMethodName(String methodName) {\n        this.methodName = methodName;\n    }\n\n    public String getFileName() {\n        return fileName;\n    }\n\n    public void setFileName(String fileName) {\n        this.fileName = fileName;\n    }\n\n    public int getLineNumber() {\n        return lineNumber;\n    }\n\n    public void setLineNumber(int lineNumber) {\n        this.lineNumber = lineNumber;\n    }\n\n    public Map<String, String> getMdc() {\n        return mdc;\n    }\n\n    public void setMdc(Map<String, String> mdc) {\n        this.mdc = mdc;\n    }\n\n    public final String getMdcValue(String key) {\n        if (key == null || mdc == null) {\n            return null;\n        }\n        return mdc.get(key);\n    }\n\n    public final String getValue(String key) {\n        if (StringUtils.isEmpty(key)) {\n            return null;\n        }\n\n        if (key.startsWith(\"x.\") && key.length() > 2) {\n            key = key.substring(2);\n            return getMdcValue(key);\n        }\n\n        try {\n            String value = BeanUtils.getProperty(this, key);\n            return value;\n        } catch (IllegalAccessException | InvocationTargetException\n                | NoSuchMethodException e) {\n            return getMdcValue(key);\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        final int prime = 31;\n        int result = 1;\n        result = prime * result + Arrays.hashCode(args);\n        result = prime * result + ((declaringClass == null) ? 0 : declaringClass.hashCode());\n        result = prime * result + ((fileName == null) ? 0 : fileName.hashCode());\n        result = prime * result + levelInt;\n        result = prime * result + lineNumber;\n        result = prime * result + ((loggerName == null) ? 0 : loggerName.hashCode());\n        result = prime * result + ((mdc == null) ? 0 : mdc.hashCode());\n        result = prime * result + ((message == null) ? 0 : message.hashCode());\n        result = prime * result + ((methodName == null) ? 0 : methodName.hashCode());\n        result = prime * result + ((serverIP == null) ? 0 : serverIP.hashCode());\n        result = prime * result + ((serverName == null) ? 0 : serverName.hashCode());\n        result = prime * result + serverPort;\n        result = prime * result + ((serviceName == null) ? 0 : serviceName.hashCode());\n        result = prime * result + ((threadName == null) ? 0 : threadName.hashCode());\n        result = prime * result + (int) (timeStamp ^ (timeStamp >>> 32));\n        return result;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj)\n            return true;\n        if (obj == null)\n            return false;\n        if (getClass() != obj.getClass())\n            return false;\n        LogItem other = (LogItem) obj;\n        if (!Arrays.equals(args, other.args))\n            return false;\n        if (declaringClass == null) {\n            if (other.declaringClass != null)\n                return false;\n        } else if (!declaringClass.equals(other.declaringClass))\n            return false;\n        if (fileName == null) {\n            if (other.fileName != null)\n                return false;\n        } else if (!fileName.equals(other.fileName))\n            return false;\n        if (levelInt != other.levelInt)\n            return false;\n        if (lineNumber != other.lineNumber)\n            return false;\n        if (loggerName == null) {\n            if (other.loggerName != null)\n                return false;\n        } else if (!loggerName.equals(other.loggerName))\n            return false;\n        if (mdc == null) {\n            if (other.mdc != null)\n                return false;\n        } else if (!mdc.equals(other.mdc))\n            return false;\n        if (message == null) {\n            if (other.message != null)\n                return false;\n        } else if (!message.equals(other.message))\n            return false;\n        if (methodName == null) {\n            if (other.methodName != null)\n                return false;\n        } else if (!methodName.equals(other.methodName))\n            return false;\n        if (serverIP == null) {\n            if (other.serverIP != null)\n                return false;\n        } else if (!serverIP.equals(other.serverIP))\n            return false;\n        if (serverName == null) {\n            if (other.serverName != null)\n                return false;\n        } else if (!serverName.equals(other.serverName))\n            return false;\n        if (serverPort != other.serverPort)\n            return false;\n        if (serviceName == null) {\n            if (other.serviceName != null)\n                return false;\n        } else if (!serviceName.equals(other.serviceName))\n            return false;\n        if (threadName == null) {\n            if (other.threadName != null)\n                return false;\n        } else if (!threadName.equals(other.threadName))\n            return false;\n        if (timeStamp != other.timeStamp)\n            return false;\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/PageResult.java",
    "content": "package com.terran4j.commons.restpack;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.util.Maths;\nimport com.terran4j.commons.util.error.BusinessException;\n\npublic class PageResult<T> {\n\n\tprivate List<T> list;\n\t\n\tprivate Integer pageIndex; \n\t\n\tprivate Integer pageSize;\n\t\n\tprivate Long total;\n\n\tpublic final List<T> getList() {\n\t\treturn list;\n\t}\n\n\tpublic final PageResult<T> setList(List<T> list) {\n\t\tthis.list = list;\n\t\treturn this;\n\t}\n\n\tpublic final Integer getPageIndex() {\n\t\treturn pageIndex;\n\t}\n\n\tpublic final PageResult<T> setPageIndex(Integer pageIndex) {\n\t\tthis.pageIndex = pageIndex;\n\t\treturn this;\n\t}\n\n\tpublic final Integer getPageSize() {\n\t\treturn pageSize;\n\t}\n\n\tpublic final PageResult<T> setPageSize(Integer pageSize) {\n\t\tthis.pageSize = pageSize;\n\t\treturn this;\n\t}\n\n\tpublic final Long getTotal() {\n\t\treturn total;\n\t}\n\n\tpublic final PageResult<T> setTotal(Long total) {\n\t\tthis.total = total;\n\t\treturn this;\n\t}\n\t\n\tpublic static interface Convertor<T, K> {\n\t\t\n\t\tK convertFrom(T from) throws BusinessException;\n\t\t\n\t}\n\t\n\tpublic <K> PageResult<K> convert(Convertor<T, K> convertor) throws BusinessException {\n\t\tPageResult<K> result = new PageResult<K>();\n\t\t\n\t\tList<K> list = new ArrayList<>(this.list.size());\n\t\tfor (T from : this.list) {\n\t\t\tK to = convertor.convertFrom(from);\n\t\t\tlist.add(to);\n\t\t}\n\t\tresult.setList(list);\n\t\t\n\t\tresult.setPageIndex(pageIndex);\n\t\tresult.setPageSize(pageSize);\n\t\tresult.setTotal(total);\n\t\t\n\t\treturn result;\n\t}\n\t\n\tpublic static final class Scope {\n\t\t\n\t\tpublic int index;\n\t\t\n\t\tpublic int count;\n\t}\n\t\n\tpublic static Scope toRecordScope(int pageIndex, int pageSize) {\n\t\treturn toRecordScope(pageIndex, pageSize, 100);\n\t}\n\t\n\tpublic static Scope toRecordScope(int pageIndex, int pageSize, int maxSize) {\n\t\tpageIndex = Maths.limitIn(pageIndex, 1, null);\n\t\tpageSize = Maths.limitIn(pageSize, 1, maxSize);\n\t\tScope scope = new Scope();\n\t\tscope.index = (pageIndex - 1) * pageSize;\n\t\tscope.count = pageSize;\n\t\treturn scope;\n\t}\n\t\n\tpublic static String asLikeContent(String fuzzy) {\n\t\tif (StringUtils.isEmpty(fuzzy)) {\n\t\t\tfuzzy = \"%\";\n\t\t} else {\n\t\t\tfuzzy = \"%\" + fuzzy.trim() + \"%\";\n\t\t}\n\t\treturn fuzzy;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/RestPackController.java",
    "content": "package com.terran4j.commons.restpack;\n\nimport static java.lang.annotation.ElementType.TYPE;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport org.springframework.web.bind.annotation.RestController;\n\n@Documented\n@Retention(RUNTIME)\n@Target(TYPE)\n@RestController\npublic @interface RestPackController {\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/RestPackIgnore.java",
    "content": "package com.terran4j.commons.restpack;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.FIELD;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n@Documented\n@Retention(RUNTIME)\n@Target(FIELD)\npublic @interface RestPackIgnore {\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/ServletUtils.java",
    "content": "package com.terran4j.commons.restpack;\n\nimport org.apache.commons.lang3.StringUtils;\nimport org.springframework.web.context.request.RequestAttributes;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n/**\n * Web请求的工具类。\n */\npublic class ServletUtils {\n\n    public static final HttpServletRequest getRequest() {\n        RequestAttributes attr = RequestContextHolder.getRequestAttributes();\n        if (attr instanceof ServletRequestAttributes) {\n            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attr;\n            return servletRequestAttributes.getRequest();\n        }\n        return null;\n    }\n\n    public static final HttpServletResponse getResponse() {\n        RequestAttributes attr = RequestContextHolder.getRequestAttributes();\n        if (attr instanceof ServletRequestAttributes) {\n            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attr;\n            return servletRequestAttributes.getResponse();\n        }\n        return null;\n    }\n\n    /**\n     * 获取客户端的真实 IP。\n     * 如果有一层代理层，也能支持，但不能支持多层代理。\n     *\n     * @return 客户端的真实 IP 。\n     */\n    public static final String getClientIP() {\n        HttpServletRequest request = getRequest();\n        if (request == null) {\n            return null;\n        }\n        String ip = request.getHeader(\"x-forwarded-for\");\n        if (StringUtils.isBlank(ip) || \"unknown\".equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"Proxy-Client-IP\");\n        }\n        if (StringUtils.isBlank(ip) || \"unknown\".equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"WL-Proxy-Client-IP\");\n        }\n        if (StringUtils.isBlank(ip) || \"unknown\".equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"HTTP_CLIENT_IP\");\n        }\n        if (StringUtils.isBlank(ip) || \"unknown\".equalsIgnoreCase(ip)) {\n            ip = request.getHeader(\"HTTP_X_FORWARDED_FOR\");\n        }\n        if (StringUtils.isBlank(ip) || \"unknown\".equalsIgnoreCase(ip)) {\n            ip = request.getRemoteAddr();\n        }\n        return ip;\n    }\n\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/config/RestPackConfiguration.java",
    "content": "package com.terran4j.commons.restpack.config;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.PropertyNamingStrategy;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.terran4j.commons.restpack.impl.*;\nimport com.terran4j.commons.restpack.log.RestPackLogAspect;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.PropertySource;\nimport org.springframework.core.convert.support.GenericConversionService;\nimport org.springframework.http.converter.HttpMessageConverter;\nimport org.springframework.http.converter.StringHttpMessageConverter;\nimport org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;\nimport org.springframework.web.bind.support.ConfigurableWebBindingInitializer;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;\nimport org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\n\n/**\n * RestPack Spring配置。\n *\n * @author wei.jiang\n */\n@PropertySource(\"classpath:restpack/freemarker.properties\")\n@EnableConfigurationProperties(HttpResultMapper.class)\n@Configuration\npublic class RestPackConfiguration extends WebMvcConfigurerAdapter {\n\n    private static final Logger log = LoggerFactory.getLogger(RestPackConfiguration.class);\n\n    private static ObjectMapper objectMapper = null;\n\n    public static final ObjectMapper getObjectMapper() {\n        if (objectMapper != null) {\n            return objectMapper;\n        }\n\n        synchronized (RestPackConfiguration.class) {\n            if (objectMapper != null) {\n                return objectMapper;\n            }\n\n            objectMapper = createObjectMapper();\n            return objectMapper;\n        }\n    }\n\n    /**\n     * RestPack 专有的 json 序列化器，与通用的有些不一样。\n     *\n     * @return json 序列化器\n     */\n    private static final ObjectMapper createObjectMapper() {\n\n        ObjectMapper objectMapper = new ObjectMapper();\n\n        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);\n\n        // 属性为空时（包括 null, 空串，空集合，空对象），不参与序列化。\n        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);\n\n        // Date 对象在序列化时，格式为 yyyy-MM-dd HH:mm:ss 。\n        // objectMapper.setDateFormat(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\"));\n        // 注意： 客户端通常喜欢接收 long 类型的时间格式，因此这里不要将日期格式化了。\n\n        objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);\n        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);\n        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);\n        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);\n\n        // json串以良好的格式输出。\n        objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);\n\n        // 当属性为空或有问题时不参与序列化。\n        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);\n\n        // 未知的属性不参与反序列化。\n        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\n\n        if (log.isInfoEnabled()) {\n            log.info(\"created ObjectMapper for RestPack.\");\n        }\n        return objectMapper;\n    }\n\n\n    @Autowired\n    private ApplicationContext applicationContext;\n\n    /**\n     * 注册一个自动将请求参数转为 Date 类型的转化器。\n     */\n//    @PostConstruct // TODO： 目前这个会影响其它功能的，暂不启用。\n    public void addConversionConfig() {\n        RequestMappingHandlerAdapter handlerAdapter = applicationContext\n                .getBean(RequestMappingHandlerAdapter.class);\n        if (handlerAdapter == null) {\n            if (log.isInfoEnabled()) {\n                log.info(\"handlerAdapter is null.\");\n            }\n            return;\n        }\n\n        ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer)\n                handlerAdapter.getWebBindingInitializer();\n        if (initializer == null) {\n            if (log.isInfoEnabled()) {\n                log.info(\"initializer is null.\");\n            }\n            return;\n        }\n\n        GenericConversionService genericConversionService =\n                (GenericConversionService) initializer.getConversionService();\n        if (genericConversionService == null) {\n            if (log.isInfoEnabled()) {\n                log.info(\"genericConversionService is null.\");\n            }\n            return;\n        }\n\n        genericConversionService.addConverter(String.class, Date.class,\n                new DateConverter());\n    }\n\n    @Bean\n    public HttpErrorHandler httpErrorHandler() {\n        return new HttpErrorHandler();\n    }\n\n    @Bean\n    public RestPackAspect restPackAspect() {\n        return new RestPackAspect();\n    }\n\n    @Bean\n    public RestPackAdvice restPackAdvice() {\n        return new RestPackAdvice();\n    }\n\n    @Bean\n    public RestPackLogAspect restPackLogAspect() {\n        return new RestPackLogAspect();\n    }\n\n    @Bean\n    public RestPackConfig restPackConfig() {\n        return new RestPackConfig();\n    }\n\n    @Bean\n    public HttpResultMapper httpResultMapper() {\n        return new HttpResultMapper();\n    }\n\n    @Override\n    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {\n\n        // 去掉 MappingJackson2HttpMessageConverter 转换器。\n        List<HttpMessageConverter<?>> removedConverters = new ArrayList<HttpMessageConverter<?>>();\n        for (HttpMessageConverter<?> converter : converters) {\n            if (converter instanceof MappingJackson2HttpMessageConverter) {\n                removedConverters.add(converter);\n            }\n        }\n        converters.removeAll(removedConverters);\n\n        // 添加 RestPackMessageConverter 转换器，并放在最高优先级。\n        HttpMessageConverter<?> restPackConverter =\n                new RestPackMessageConverter(getObjectMapper());\n        converters.add(0, restPackConverter);\n    }\n\n}"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/DateConverter.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport org.springframework.core.convert.converter.Converter;\nimport org.springframework.util.StringUtils;\n\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\n\npublic class DateConverter implements Converter<String, Date> {\n\n    public static final String DATE_FORMAT = \"yyyy-MM-dd HH:mm:ss\";\n\n    @Override\n    public Date convert(String source) {\n        Date date = null;\n        if (StringUtils.isEmpty(source)) {\n            return date;\n        }\n        source = source.trim();\n\n        try {\n            long time = Long.parseLong(source);\n            date = new Date(time);\n            return date;\n        } catch (NumberFormatException e) {\n            // ignore.\n        }\n\n        SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);\n        try {\n            date = sdf.parse(source);\n            return date;\n        } catch (ParseException e) {\n            String msg = String.format(\"Parse Date value[%s] error, \" +\n                    \"must be format: %s\", source, DATE_FORMAT);\n            throw new RuntimeException(msg);\n        }\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/ExceptionHolder.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\npublic class ExceptionHolder {\n\n    private static final ThreadLocal<Exception> localException = new ThreadLocal<>();\n\n    public static final void set(Exception e) {\n        localException.set(e);\n    }\n\n    public static final Exception get() {\n        return localException.get();\n    }\n\n    public static final void remove() {\n        localException.remove();\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/HttpAroundHandler.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport com.terran4j.commons.restpack.HttpResult;\nimport org.aspectj.lang.ProceedingJoinPoint;\n\n/**\n * \n * @author wei.jiang\n */\npublic interface HttpAroundHandler {\n\n\t/**\n\t * 此处理器的优先级，越小优先级越高。<br>\n\t * 当执行 preHandle 方法时，优先级越高的越先执行。<br>\n\t * 当执行 postHandle 方法时，优先级越高的越后执行，与 preHandle 时正好反过来了。<br>\n\t * \n\t * @return\n\t */\n\tint getPriority();\n\n\t/**\n\t * 请求前预处理。\n\t * \n\t * @param point\n\t * @param request\n\t * @param response\n\t * @return 为 null 表示后续继续执行，为不 null 表示后续不再执行，返回的是这个结果。\n\t */\n\tHttpResult preHandle(ProceedingJoinPoint point, HttpServletRequest request, HttpServletResponse response);\n\n\t/**\n\t * 请求后后处理。\n\t * \n\t * @param point\n\t * @param request\n\t * @param response\n\t * @param result\n\t * @return 为 null 表示后续继续执行，为不 null 表示后续不再执行，返回的是这个结果。\n\t */\n\tHttpResult postHandle(ProceedingJoinPoint point, HttpServletRequest request, HttpServletResponse response,\n\t\t\tHttpResult result);\n\n}"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/HttpErrorHandler.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport com.terran4j.commons.util.error.AuthorizedException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.MDC;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.bind.MissingServletRequestParameterException;\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseBody;\nimport org.springframework.web.bind.annotation.ResponseStatus;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\n\n@ControllerAdvice\npublic class HttpErrorHandler {\n\n    private static final Logger log = LoggerFactory.getLogger(HttpErrorHandler.class);\n\n    @Autowired\n    private HttpResultMapper httpResultMapper;\n\n    @ResponseBody\n    @ExceptionHandler(MissingServletRequestParameterException.class)\n    @ResponseStatus(HttpStatus.OK)\n    public Object handleMissingServletRequestParameterException(\n            MissingServletRequestParameterException e, HttpServletRequest request) {\n        return toHttpResult(e, request);\n    }\n\n    @ResponseBody\n    @ExceptionHandler(AuthorizedException.class)\n    @ResponseStatus(HttpStatus.UNAUTHORIZED)\n    public Object handleAuthorizedException(Exception e, HttpServletRequest request) {\n        return toHttpResult(e, request);\n    }\n\n    @ResponseBody\n    @ExceptionHandler(Exception.class)\n    @ResponseStatus(HttpStatus.BAD_REQUEST)\n    public Object handleAllException(Exception e, HttpServletRequest request) {\n        return toHttpResult(e, request);\n    }\n\n    private Object toHttpResult(Exception e, HttpServletRequest request) {\n\n        long t0 = System.currentTimeMillis();\n        if (request == null) {\n            ServletRequestAttributes servlet = (ServletRequestAttributes)\n                    RequestContextHolder.getRequestAttributes();\n            request = servlet.getRequest();\n        }\n\n        String requestId = RestPackAspect.generateRequestId();\n        String requestPath = request.getRequestURI();\n        MDC.put(\"requestId\", requestId);\n        MDC.put(\"requestPath\", requestPath);\n\n        BusinessException be;\n        if (e instanceof MissingServletRequestParameterException) {\n            MissingServletRequestParameterException me = (MissingServletRequestParameterException) e;\n            log.info(\"missing param: {} in path: {}\", me.getParameterName(), request.getPathInfo());\n            be = new BusinessException(ErrorCodes.NULL_PARAM)\n                    .put(\"key\", me.getParameterName())\n                    .setMessage(\"参数 ${key} 不能为空！\");\n        }else if(e instanceof BusinessException){\n            be = (BusinessException) e;\n            be.setLocale(request.getLocale());\n            log.info(\"[Handled] business exception message {}\", be.getMessage());\n        }else {\n            log.error(\"[Handled] unknown exception\", e);\n            be = new BusinessException(CommonErrorCode.UNKNOWN_ERROR, e)\n                    .setMessage(e.getClass().getName() + \": \" + e.getMessage());\n        }\n\n        HttpResult result = HttpResult.fail(be);\n        result.setRequestId(requestId);\n        long t = System.currentTimeMillis();\n        result.setServerTime(t);\n        result.setSpendTime(t - t0);\n        return httpResultMapper.convert(result);\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/HttpResultMapper.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.restpack.HttpResultConverter;\nimport com.terran4j.commons.restpack.LogItem;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.stereotype.Component;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n@ConfigurationProperties(prefix = \"terran4j.restpack.renaming\")\n@Component\npublic class HttpResultMapper {\n\n    private String requestId = \"requestId\";\n\n    private String serverTime = \"serverTime\";\n\n    private String spendTime = \"spendTime\";\n\n    private String resultCode = \"resultCode\";\n\n    private String data = \"data\";\n\n    private String message = \"message\";\n\n    private String props = \"props\";\n\n    private String logs = \"logs\";\n\n    private String success = \"success\";\n\n    public String getRequestId() {\n        return requestId;\n    }\n\n    public void setRequestId(String requestId) {\n        this.requestId = requestId;\n    }\n\n    public String getServerTime() {\n        return serverTime;\n    }\n\n    public void setServerTime(String serverTime) {\n        this.serverTime = serverTime;\n    }\n\n    public String getSpendTime() {\n        return spendTime;\n    }\n\n    public void setSpendTime(String spendTime) {\n        this.spendTime = spendTime;\n    }\n\n    public String getResultCode() {\n        return resultCode;\n    }\n\n    public void setResultCode(String resultCode) {\n        this.resultCode = resultCode;\n    }\n\n    public String getData() {\n        return data;\n    }\n\n    public void setData(String data) {\n        this.data = data;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n    public String getProps() {\n        return props;\n    }\n\n    public void setProps(String props) {\n        this.props = props;\n    }\n\n    public String getSuccess() {\n        return success;\n    }\n\n    public void setSuccess(String success) {\n        this.success = success;\n    }\n\n    public Map<String, Object> toMap(HttpResult httpResult) {\n        if (httpResult == null) {\n            throw new NullPointerException(\"httpResult is null.\");\n        }\n        Map<String, Object> map = new HashMap<>();\n\n        String requestIdValue = httpResult.getRequestId();\n        if (requestIdValue != null) {\n            map.put(requestId, requestIdValue);\n        }\n\n        long serverTimeValue = httpResult.getServerTime();\n        map.put(serverTime, serverTimeValue);\n\n        long spendTimeValue = httpResult.getSpendTime();\n        map.put(spendTime, spendTimeValue);\n\n        String resultCodeValue = httpResult.getResultCode();\n        if (resultCodeValue != null) {\n            if (resultCodeValue.equals(HttpResult.SUCCESS_CODE)) {\n                resultCodeValue = success;\n            }\n            map.put(resultCode, resultCodeValue);\n        }\n\n        Object dataValue = httpResult.getData();\n        if (dataValue != null) {\n            map.put(data, dataValue);\n        }\n\n        String messageValue = httpResult.getMessage();\n        if (messageValue != null) {\n            map.put(message, messageValue);\n        }\n\n        Map<String, Object> propsValue = httpResult.getProps();\n        if (propsValue != null) {\n            map.put(props, propsValue);\n        }\n\n        List<String> logLists = httpResult.getLogs();\n        if (logLists != null) {\n            map.put(logs, logLists);\n        }\n\n        return map;\n    }\n\n    @Autowired(required = false)\n    private HttpResultConverter httpResultConverter;\n\n    public Object convert(HttpResult httpResult) {\n        if (httpResultConverter != null) {\n            return httpResultConverter.convert(httpResult);\n        } else {\n            return toMap(httpResult);\n        }\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/RestPackAdvice.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport ch.qos.logback.classic.PatternLayout;\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.restpack.log.RestPackLogAppender;\nimport com.terran4j.commons.restpack.LogItem;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.slf4j.Logger;\nimport org.slf4j.MDC;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.core.MethodParameter;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.converter.HttpMessageConverter;\nimport org.springframework.http.server.ServerHttpRequest;\nimport org.springframework.http.server.ServerHttpResponse;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.bind.MissingServletRequestParameterException;\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;\n\nimport java.util.List;\n\n/**\n * 本类负责将原始返回值、或异常类包裹在 HttpResult 对象中。\n *\n * @author jiangwei\n */\n@Component\n@ControllerAdvice\npublic class RestPackAdvice implements ResponseBodyAdvice<Object> {\n\n    @Autowired\n    private HttpResultMapper httpResultMapper;\n\n    @Override\n    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {\n        return RestPackAspect.isRestPack();\n    }\n\n    @Override\n    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,\n                                  Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,\n                                  ServerHttpResponse response) {\n\n        // 不是 RestPack 处理的请求，就不需要特殊处理。\n        if (!RestPackAspect.isRestPack()) {\n            return body;\n        }\n\n        HttpResult result;\n        Exception e = ExceptionHolder.get();\n        if (e != null) {\n            BusinessException be = convert(e);\n            result = HttpResult.fail(be);\n        } else {\n            // 将原始返回值包裹在 HttpResult 对象中。\n            result = HttpResult.success();\n            if (body != null) {\n                RestPackUtils.clearIgnoreFields(body);\n                result.setData(body);\n            }\n        }\n        setHttpResult(result);\n\n        Logger log = RestPackAspect.getLog();\n        if (log != null && log.isInfoEnabled()) {\n            log.info(\"request '{}' end, response:\\n{}\", MDC.get(\"requestPath\"), result);\n        }\n\n        RestPackAspect.clearThreadLocal();\n\n        return httpResultMapper.convert(result);\n    }\n\n\n    void setHttpResult(HttpResult result) {\n        String requestId = RestPackAspect.getRequestId();\n        if (requestId != null) {\n            result.setRequestId(requestId);\n        }\n\n        Long beginTime = RestPackAspect.getBeginTime();\n        if (beginTime != null) {\n            result.setServerTime(beginTime);\n            long spendTime = System.currentTimeMillis() - beginTime;\n            result.setSpendTime(spendTime);\n        }\n\n        List<String> logs = RestPackLogAppender.getLogs();\n        result.setLogs(logs);\n    }\n\n    BusinessException convert(Throwable e) {\n        if (e instanceof BusinessException) {\n            return (BusinessException) e;\n        }\n\n        if (e instanceof MissingServletRequestParameterException) {\n            MissingServletRequestParameterException me = (MissingServletRequestParameterException) e;\n            String paramKey = me.getParameterName();\n            String paramType = me.getParameterType();\n\n            Logger log = RestPackAspect.getLog();\n            if (log != null && log.isInfoEnabled()) {\n                log.info(\"miss param, key = {}, type = {}\", paramKey, paramType);\n            }\n\n            return new BusinessException(ErrorCodes.NULL_PARAM)\n                    .put(\"key\", paramKey);\n        }\n\n        // Error 没有办法拦截，这里只能日志记录异常信息。\n        if (e instanceof Error) {\n            e.printStackTrace();\n\n            Logger log = RestPackAspect.getLog();\n            if (log != null) {\n                log.error(e.getMessage(), e);\n            }\n        }\n\n        return new BusinessException(ErrorCodes.UNKNOWN_ERROR, e)\n                .setMessage(e.getClass().getName() + \": \" + e.getMessage());\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/RestPackAspect.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport com.terran4j.commons.restpack.RestPackController;\nimport com.terran4j.commons.restpack.log.RestPackLogAppender;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.Strings;\nimport org.aspectj.lang.JoinPoint;\nimport org.aspectj.lang.annotation.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.MDC;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.context.request.RequestAttributes;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\n/**\n * 本切面拦截有<code>@RequestMapping</code>注解，\n * 并且类上有<code>HttpResultPackController</code>注解的方法：<br>\n * 1. 生成并记录 requestId.<br>\n * 2. 记录开始时间。<br>\n * 3. 记录异常对象。\n */\n@Aspect\n@Order(1)\n@Component\npublic class RestPackAspect {\n\n    private static final ThreadLocal<String> bufferRequestId = new ThreadLocal<>();\n\n    private static final ThreadLocal<Long> bufferBeginTime = new ThreadLocal<>();\n\n    private static final ThreadLocal<Logger> bufferLog = new ThreadLocal<>();\n\n    public static final boolean isRestPack() {\n        return bufferRequestId.get() != null;\n    }\n\n    public static final String getRequestId() {\n        return bufferRequestId.get();\n    }\n\n    public static final Long getBeginTime() {\n        return bufferBeginTime.get();\n    }\n\n    public static final Logger getLog() {\n        return bufferLog.get();\n    }\n\n//    @Autowired\n//    private RestPackConfig restPackConfig;\n\n    public RestPackAspect() {\n        super();\n    }\n\n    @Pointcut(\"@annotation(org.springframework.web.bind.annotation.RequestMapping)\")\n    public void doRequestMapping() {\n    }\n\n    @Pointcut(\"@annotation(org.springframework.web.bind.annotation.GetMapping)\")\n    public void doGetMapping() {\n    }\n\n    @Pointcut(\"@annotation(org.springframework.web.bind.annotation.PostMapping)\")\n    public void doPostMapping() {\n    }\n\n    @Pointcut(\"@annotation(org.springframework.web.bind.annotation.PutMapping)\")\n    public void doPutMapping() {\n    }\n\n    @Pointcut(\"@annotation(org.springframework.web.bind.annotation.DeleteMapping)\")\n    public void doDeleteMapping() {\n    }\n\n    @Pointcut(\"@annotation(org.springframework.web.bind.annotation.PatchMapping)\")\n    public void doPatchMapping() {\n    }\n\n    private static final String POINTCUT_EXP = \"doRequestMapping() \" +\n            \"|| doGetMapping() || doPostMapping() || doPutMapping()\" +\n            \"|| doDeleteMapping() || doPatchMapping()\";\n\n    @After(POINTCUT_EXP)\n    public void doAfter(JoinPoint point) {\n        if (isRestPack()) {\n            RequestAttributes servlet = RequestContextHolder.getRequestAttributes();\n            HttpServletResponse response = ((ServletRequestAttributes) servlet).getResponse();\n            response.setCharacterEncoding(\"UTF-8\");\n            response.setHeader(\"Content-Type\", \"text/json;charset=UTF-8\");\n        }\n    }\n\n    /**\n     * 记录请求日志、requestId、开始处理时间。\n     */\n    @Before(POINTCUT_EXP)\n    public void doBefore(JoinPoint point) {\n        ServletRequestAttributes servlet = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n        HttpServletRequest request = servlet.getRequest();\n        String requestId = generateRequestId();\n        String requestPath = request.getRequestURI();\n        MDC.put(\"requestId\", requestId);\n        MDC.put(\"requestPath\", requestPath);\n\n        // 写入 ThreadLocal 数据之前，先清除以前的历史数据（如果有的话）。\n        clearThreadLocal();\n\n        // 如果探测到 RestPackLogAppender 被启用，才启用 debug 模式\n        if (RestPackLogAppender.isActive()) {\n            String logEnabled = request.getHeader(RestPackLogAppender.KEY_LOG_ENABLED);\n            if (\"true\".equals(logEnabled)) {\n                String logPattern = request.getHeader(RestPackLogAppender.KEY_LOG_PATTERN);\n                RestPackLogAppender.logEnabled(logPattern);\n            }\n        }\n\n        // 只有类上或父类上有 @ResultPackController 注解的，才需要打包返回结果。\n        Object target = point.getTarget();\n        Class<?> clazz = Classes.getTargetClass(target);\n        RestPackController pack = clazz.getAnnotation(RestPackController.class);\n        if (pack != null) {\n            long beginTime = System.currentTimeMillis();\n            bufferBeginTime.set(beginTime);\n            bufferRequestId.set(requestId);\n\n            Logger log = LoggerFactory.getLogger(clazz);\n            bufferLog.set(log);\n        }\n\n        Logger log = bufferLog.get();\n        if (log != null && log.isInfoEnabled()) {\n            Map<String, Object> params = new HashMap<>();\n            Enumeration<String> it = request.getParameterNames();\n            while (it.hasMoreElements()) {\n                String key = it.nextElement();\n                String value = request.getParameter(key);\n                if (value != null) {\n                    params.put(key, value);\n                }\n                String[] values = request.getParameterValues(key);\n                if (values != null && values.length > 1) {\n                    params.put(key, values);\n                }\n            }\n            if (log.isInfoEnabled()) {\n                log.info(\"request '{}' begin, params:\\n{}\",\n                        requestPath, Strings.toString(params));\n            }\n        }\n    }\n\n    /**\n     * 记录异常对象，以便于后续处理转化成<code>HttpResult</code>对象。\n     * @param e 异常对象\n     */\n    @AfterThrowing(pointcut = POINTCUT_EXP, throwing = \"e\")\n    public void handleThrowing(Exception e) {\n        Logger log = bufferLog.get();\n        if (log != null && log.isInfoEnabled()) {\n            log.info(\"handle throw exception[{}]: {}\",\n                    e.getClass().getName(), e.getMessage());\n        }\n        ExceptionHolder.set(e);\n    }\n\n    public static String generateRequestId() {\n        return UUID.randomUUID().toString().replace(\"-\",\n                \"\");\n    }\n\n    static void clearThreadLocal() {\n        bufferRequestId.remove();\n        bufferBeginTime.remove();\n        ExceptionHolder.remove();\n        bufferLog.remove();\n        RestPackLogAppender.logClear();\n    }\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/RestPackConfig.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class RestPackConfig {\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/RestPackMessageConverter.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport org.springframework.http.MediaType;\nimport org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\npublic class RestPackMessageConverter extends MappingJackson2HttpMessageConverter {\n\n\tpublic RestPackMessageConverter(ObjectMapper objectMapper) {\n\t\tsuper(objectMapper);\n\t}\n\n\t@Override\n\tpublic boolean canWrite(Class<?> clazz, MediaType mediaType) {\n\t\tif (RestPackAspect.isRestPack()) {\n\t\t\treturn true;\n\t\t}\n\t\treturn super.canWrite(clazz, mediaType);\n\t}\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/impl/RestPackUtils.java",
    "content": "package com.terran4j.commons.restpack.impl;\n\nimport com.terran4j.commons.restpack.RestPackIgnore;\nimport com.terran4j.commons.util.Beans;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.BeanUtils;\n\nimport java.beans.PropertyDescriptor;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.*;\n\npublic class RestPackUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(RestPackUtils.class);\n\n    private static Set<Class<?>> basicClasses = new HashSet<Class<?>>();\n\n    static {\n        basicClasses.add(boolean.class);\n        basicClasses.add(byte.class);\n        basicClasses.add(char.class);\n        basicClasses.add(short.class);\n        basicClasses.add(int.class);\n        basicClasses.add(long.class);\n        basicClasses.add(float.class);\n        basicClasses.add(double.class);\n    }\n\n    private static Set<Class<?>> javaClasses = new HashSet<Class<?>>();\n\n    static {\n        javaClasses.add(Boolean.class);\n        javaClasses.add(Byte.class);\n        javaClasses.add(Character.class);\n        javaClasses.add(Short.class);\n        javaClasses.add(Integer.class);\n        javaClasses.add(Long.class);\n        javaClasses.add(Float.class);\n        javaClasses.add(Double.class);\n        javaClasses.add(String.class);\n        javaClasses.add(Date.class);\n        javaClasses.add(Class.class);\n    }\n\n    public static final boolean isBasicType(Class<?> clazz) {\n        return basicClasses.contains(clazz);\n    }\n\n    public static final boolean isJavaType(Class<?> clazz) {\n        return javaClasses.contains(clazz);\n    }\n\n    public static void clearIgnoreFields(Object bean) {\n        if (bean == null) {\n            return;\n        }\n\n        Class<?> beanClass = bean.getClass();\n        if (isJavaType(beanClass)) {\n            return;\n        }\n\n        // 对于集合类型，要处理每一个子元素对象。\n        if (bean instanceof Collection) {\n            Collection collection = (Collection) bean;\n            for (Object element : collection) {\n                clearIgnoreFields(element);\n            }\n            return;\n        }\n\n        // 对于 Map 类型，要处理每一个值对象。\n        if (bean instanceof Map) {\n            Map map = (Map) bean;\n            clearIgnoreFields(map.values());\n            return;\n        }\n\n        // 处理每个属性字段。\n        PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(beanClass);\n        if (propertyDescriptors == null || propertyDescriptors.length == 0) {\n            return;\n        }\n        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {\n            String fieldName = propertyDescriptor.getName();\n\n            // 必须满足 JavaBean 规范,要有 setter / getter 方法，还要有字段定义。\n            Method getMethod = propertyDescriptor.getReadMethod();\n            Method setMethod = propertyDescriptor.getWriteMethod();\n            if (getMethod == null || setMethod == null) {\n                continue;\n            }\n            Field field = null;\n            try {\n                field = beanClass.getDeclaredField(fieldName);\n            } catch (NoSuchFieldException e) {\n                // ignore it.\n            }\n            if (field == null) {\n                continue;\n            }\n\n            RestPackIgnore restPackIgnore = field.getAnnotation(RestPackIgnore.class);\n            if (restPackIgnore != null) {\n                Class<?> fieldType = field.getType();\n                if (isBasicType(fieldType)) {\n                    String msg = String.format(\"@%s 不允许修饰在基本类型字段上\\n字段： %s %s \\n类：%s\",\n                            RestPackIgnore.class.getSimpleName(), fieldType.getSimpleName(), fieldName,\n                            beanClass.getName());\n                    throw new RuntimeException(msg);\n                }\n                try {\n                    Beans.setFieldValue(bean, fieldName, null);\n                } catch (Exception e) {\n                    throw new RuntimeException(\"清除 RestPackIgnore 字段值出错： \" + e.getMessage(), e);\n                }\n            } else {\n                Object fieldValue = null;\n                try {\n                    fieldValue = getMethod.invoke(bean);\n                } catch (IllegalAccessException | InvocationTargetException e) {\n                    String msg = String.format(\"读取字段[ %s#%s ]的值出错: %s\",\n                            beanClass.getName(), fieldName, e.getMessage());\n                    throw new RuntimeException(msg);\n                }\n                clearIgnoreFields(fieldValue);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/log/RestPackLogAppender.java",
    "content": "package com.terran4j.commons.restpack.log;\n\nimport ch.qos.logback.classic.LoggerContext;\nimport ch.qos.logback.classic.PatternLayout;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.UnsynchronizedAppenderBase;\nimport com.terran4j.commons.restpack.Log;\nimport com.terran4j.commons.restpack.LogItem;\nimport org.springframework.util.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Queue;\n\npublic class RestPackLogAppender<E> extends UnsynchronizedAppenderBase<E> {\n\n    public static final String KEY_LOG_PATTERN = \"logPattern\";\n\n    public static final String KEY_LOG_ENABLED = \"logEnabled\";\n\n    private static boolean active = false;\n\n    private static final ThreadLocal<LinkedList<ILoggingEvent>> logs = new ThreadLocal<>();\n\n    private static final ThreadLocal<String> threadLogPattern = new ThreadLocal<>();\n\n    public static boolean isActive() {\n        return active;\n    }\n\n    public static void logEnabled(String logPattern) {\n        // 都用了 ThreadLocal 了，不会有多线程并发的问题，因此不用带并发控制的 Queue 。\n        LinkedList<ILoggingEvent> logQueue = new LinkedList<>();\n        logs.set(logQueue);\n        threadLogPattern.set(logPattern);\n    }\n\n    public static void logClear() {\n        logs.set(null);\n        threadLogPattern.set(null);\n    }\n\n//    public static LogItem[] getLogItems() {\n//        Queue<ILoggingEvent> queue = logs.get();\n//        if (queue == null || queue.size() == 0) {\n//            return null;\n//        }\n//        return queue.toArray(new LogItem[queue.size()]);\n//    }\n\n    public static List<String> getLogs() {\n        LinkedList<ILoggingEvent> queue = logs.get();\n        if (queue == null || queue.size() == 0) {\n            return null;\n        }\n\n        String logPattern = threadLogPattern.get();\n        PatternLayout layout = new PatternLayout();\n        if (StringUtils.isEmpty(logPattern)) {\n            logPattern = \"%date %level -- %-40logger{35}[%line]:%n%msg%n\";\n        }\n        layout.setPattern(logPattern);\n        LoggerContext defaultLoggerContext = new LoggerContext();\n        layout.setContext(defaultLoggerContext);\n        layout.start();\n\n        List<String> messages = new ArrayList<>();\n        for (ILoggingEvent event : queue) {\n            String msg = layout.doLayout(event);\n            messages.add(msg);\n        }\n        return messages;\n    }\n\n    @Override\n    protected void append(E eventObject) {\n        if (!isStarted()) {\n            return;\n        }\n\n        // 如果程序能执行到这来，说明这个 Appender 是启用的。\n        // 这样就可能判断自己是想希望在程序中启用 debug 功能。\n        active = true;\n\n        // 只是说明本次请求未启用 RestPackLog 功能。\n        Queue<ILoggingEvent> logItemQueue = logs.get();\n        if (logItemQueue == null) {\n            return;\n        }\n\n        if (eventObject instanceof ILoggingEvent) {\n            ILoggingEvent logEvent = (ILoggingEvent) eventObject;\n\n            // 本功能产生的日志忽略，以免引起循环调用。\n            String loggerName = logEvent.getLoggerName();\n            if (loggerName.startsWith(Log.class.getPackage().getName())) {\n                return;\n            }\n\n            // 记录日志。\n//            ILoggingEvent logItem = LogItem.fromEvent(logEvent);\n            logItemQueue.add(logEvent);\n        }\n    }\n\n}"
  },
  {
    "path": "commons-restpack/src/main/java/com/terran4j/commons/restpack/log/RestPackLogAspect.java",
    "content": "package com.terran4j.commons.restpack.log;\n\nimport com.terran4j.commons.restpack.Log;\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.Strings;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.aspectj.lang.annotation.Around;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport org.springframework.util.StringUtils;\n\nimport java.lang.reflect.Method;\n\n/**\n * 对有 @Log 的方法调用进行拦截，以记录日志。\n *\n * @author wei.jiang\n */\n@Aspect\n@Order(100)\n@Component\npublic class RestPackLogAspect {\n\n    @Around(\"@annotation(com.terran4j.commons.restpack.Log)\")\n    public Object doLog(ProceedingJoinPoint point) throws Throwable {\n\n        Object target = point.getTarget();\n        Class<?> targetClass = Classes.getTargetClass(target);\n        String methodName = point.getSignature().getName();\n        Object[] args = point.getArgs();\n        Method method = Classes.getMethod(targetClass, methodName, args, Log.class);\n\n        // 没有 @Log 注释，让方法正常执行。\n        if (method == null) {\n            return point.proceed(args);\n        }\n\n        // 每个类的日志对象，slf4j 已经缓存了，这里不需要再缓存。\n        Logger log = LoggerFactory.getLogger(targetClass);\n\n        // 操作的名称，如果没有定义就用\"类名::方法名\"\n        Log logAnno = method.getAnnotation(Log.class);\n        String actionName = targetClass.getSimpleName() + \"::\" + methodName;\n        if (logAnno != null && StringUtils.hasText(logAnno.value())) {\n            actionName = logAnno.value();\n        }\n\n        // 记录一个方法调用的开始。\n        if (log != null && log.isInfoEnabled()) {\n            if (args != null && args.length > 0) {\n                String argsText = Strings.toString(args);\n                if (argsText.length() > 100) {\n                    argsText = argsText.substring(0, 100) + \"......\";\n                }\n                log.info(\"{} begin, args = {}\", actionName, argsText);\n            } else {\n                log.info(\"{} begin.\", actionName);\n            }\n        }\n\n        long beginTime = System.currentTimeMillis();\n        Object response = point.proceed(args); // 执行服务\n        long endTime = System.currentTimeMillis();\n        long spendTime = endTime - beginTime; // 统计运行时间。\n\n        // 记录一个方法调用的结束。\n        if (log != null && log.isInfoEnabled()) {\n            if (response != null) {\n                String responseText = Strings.toString(response);\n                if (responseText.length() > 100) {\n                    responseText = responseText.substring(0, 100) + \"......\";\n                }\n                log.info(\"{} end, returnObject = {}\", actionName, responseText);\n            }\n            log.info(\"{} end, spend {}ms\", actionName, spendTime);\n        }\n\n        return response;\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/main/resources/restpack/freemarker.properties",
    "content": "\n\nspring.freemarker.allow-request-override = false\nspring.freemarker.cache = false\nspring.freemarker.check-template-location = true\nspring.freemarker.charset = UTF-8\nspring.freemarker.content-type = text/html\nspring.freemarker.expose-request-attributes = false\nspring.freemarker.expose-session-attributes = false\nspring.freemarker.expose-spring-macro-helpers = false\nspring.freemarker.prefer-file-system-access = false\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/demo/restpack/DemoHttpResultConverter.java",
    "content": "package com.terran4j.demo.restpack;\n\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.restpack.HttpResultConverter;\nimport org.springframework.stereotype.Service;\n\n@Service\npublic class DemoHttpResultConverter implements HttpResultConverter {\n\n    @Override\n    public Object convert(HttpResult httpResult) {\n        // 这里可以将 HttpResult 对象转成你需要的格式\n        // RestPack 框架会将本方法的返回对象转成 JSON 串返回给请求方。\n        return httpResult;\n    }\n}"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/demo/restpack/HelloBean.java",
    "content": "package com.terran4j.demo.restpack;\n\nimport com.terran4j.commons.restpack.RestPackIgnore;\nimport com.terran4j.commons.util.Strings;\n\nimport java.util.Date;\n\npublic class HelloBean {\n\t\n\tprivate String name;\n\t\n\tprivate String message;\n\t\n\tprivate Date time;\n\n\t@RestPackIgnore\n\tprivate Boolean deleted = false;\n\n    @RestPackIgnore\n\tprivate long count = 1;\n\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\n\tpublic final void setName(String name) {\n\t\tthis.name = name;\n\t}\n\n\tpublic final String getMessage() {\n\t\treturn message;\n\t}\n\n\tpublic final void setMessage(String message) {\n\t\tthis.message = message;\n\t}\n\n\tpublic final Date getTime() {\n\t\treturn time;\n\t}\n\n\tpublic final void setTime(Date time) {\n\t\tthis.time = time;\n\t}\n\n\tpublic Boolean getDeleted() {\n\t\treturn deleted;\n\t}\n\n\tpublic void setDeleted(Boolean deleted) {\n\t\tthis.deleted = deleted;\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn Strings.toString(this);\n\t}\n\t\n}"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/demo/restpack/HelloController.java",
    "content": "package com.terran4j.demo.restpack;\n\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestParam;\n\nimport java.util.Map;\n\n@Controller\npublic class HelloController {\n\n    @RequestMapping(\"/restpack/hello.html\")\n    public String hello(\n            @RequestParam(value = \"name\", defaultValue = \"world\") String name,\n            Map<String, Object> model) {\n        model.put(\"name\", name);\n        return \"restpack/hello\";\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/demo/restpack/RestPackDemoApp.java",
    "content": "package com.terran4j.demo.restpack;\n\nimport com.terran4j.commons.restpack.EnableRestPack;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@Slf4j\n@EnableRestPack\n@SpringBootApplication\npublic class RestPackDemoApp {\n\n    public static void main(String[] args) {\n        if (log.isInfoEnabled()) {\n            log.info(\"Starting RestPackDemoApp...\");\n        }\n        SpringApplication.run(RestPackDemoApp.class, args);\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/demo/restpack/RestPackDemoAspect.java",
    "content": "package com.terran4j.demo.restpack;\n\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.aspectj.lang.JoinPoint;\nimport org.aspectj.lang.annotation.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.core.annotation.Order;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.context.request.RequestAttributes;\nimport org.springframework.web.context.request.RequestContextHolder;\nimport org.springframework.web.context.request.ServletRequestAttributes;\n\nimport javax.servlet.http.HttpServletRequest;\n\n@Aspect\n@Order(100)\n@Component\npublic class RestPackDemoAspect {\n\n    private static final Logger log = LoggerFactory.getLogger(RestPackDemoAspect.class);\n\n    public RestPackDemoAspect() {\n        super();\n    }\n\n    @Pointcut(\"@annotation(org.springframework.web.bind.annotation.RequestMapping)\")\n    public void httpDemoPackAspect() {\n    }\n\n    @After(\"httpDemoPackAspect()\")\n    public void doAfter(JoinPoint point) {\n    }\n\n    @Before(\"httpDemoPackAspect()\")\n    public void doBefore(JoinPoint point) throws BusinessException {\n        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();\n        ServletRequestAttributes servlet = (ServletRequestAttributes) requestAttributes;\n        HttpServletRequest httpRequest = servlet.getRequest();\n        String f = httpRequest.getParameter(\"f\");\n        if (\"e\".equals(f)) {\n            throw new BusinessException(ErrorCodes.ACCESS_DENY)\n                    .setMessage(\"不允许执行的操作!\");\n        }\n    }\n\n//    @AfterThrowing(pointcut = \"httpDemoPackAspect()\", throwing = \"e\")\n//    public void handleThrowing(Exception e) {\n//        if (log.isInfoEnabled()) {\n//            log.info(\"handle throwed exception[{}]: {}\", e.getClass().getName(), e.getMessage());\n//        }\n//        ExceptionHolder.set(e);\n//    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/demo/restpack/RestPackDemoController.java",
    "content": "package com.terran4j.demo.restpack;\n\nimport com.terran4j.commons.restpack.Log;\nimport com.terran4j.commons.restpack.RestPackController;\nimport com.terran4j.commons.restpack.ServletUtils;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.bind.annotation.*;\n\nimport java.util.Date;\n\n@RestPackController\n@RequestMapping(\"/demo/restpack\")\npublic class RestPackDemoController {\n\n    private static final Logger log = LoggerFactory.getLogger(RestPackDemoController.class);\n\n    @Log(\"doEcho\")\n    private String doEcho(String msg) {\n        return msg;\n    }\n\n    /**\n     * http://localhost:8080/demo/restpack/echo?msg=abc\n     * curl -H \"DEBUG=true\" \"http://localhost:8080/demo/restpack/echo?msg=abc\"\n     * TODO: 不知为什么，当使用 @GetMapping 时，框架不能自动设置\n     * Content-Type 为 application/json;charset=UTF-8 ，\n     * 因此这里用 produces 参数手动设置一下。\n     */\n    @Log(\"echo\")\n    @GetMapping(value = \"/echo\", produces = \"application/json;charset=UTF-8\")\n    public String echo(@RequestParam(value = \"msg\") String msg) throws BusinessException {\n        return doEcho(msg);\n    }\n\n    /**\n     * http://127.0.0.1:8080/demo/restpack/client/ip\n     */\n    @RequestMapping(value = \"/client/ip\", method = RequestMethod.GET)\n    public String clientIP() throws BusinessException {\n        return ServletUtils.getClientIP();\n    }\n\n    /**\n     * http://localhost:8080/demo/restpack/date?time=1522812467402\n     */\n    @Log\n    @RequestMapping(value = \"/date\", method = RequestMethod.GET)\n    public Date toDate(long time) throws BusinessException {\n        Date date = new Date(time);\n        log.info(\"echo, date = {}\", date);\n        return new Date(date.getTime() + 1000);\n    }\n\n    /**\n     * curl -d \"msg=abc\" \"http://localhost:8080/demo/restpack/void\"\n     *\n     * @param msg\n     * @throws BusinessException\n     */\n    @Log\n    @PostMapping(value = \"/void\")\n    public void doVoid(@RequestParam(value = \"msg\") String msg) throws BusinessException {\n        log.info(\"doVoid, msg = {}\", msg);\n    }\n\n    /**\n     * http://localhost:8080/demo/restpack/hello?name=neo\n     *\n     * @param name\n     * @throws BusinessException\n     */\n    @Log\n    @RequestMapping(value = \"/hello\", method = RequestMethod.GET)\n    public HelloBean hello(String name,\n                           @RequestParam(required = false) Date date)\n            throws BusinessException {\n        log.info(\"hello, name = {}\", name);\n        HelloBean bean = new HelloBean();\n        bean.setName(name);\n        bean.setMessage(\"Hello, \" + name + \"!\");\n        if (date != null) {\n            bean.setTime(new Date(date.getTime() + 1000));\n        }\n        return bean;\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/demo/restpack/RestPackErrorController.java",
    "content": "package com.terran4j.demo.restpack;\n\nimport com.terran4j.commons.restpack.RestPackController;\nimport com.terran4j.commons.util.error.BusinessException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.RequestParam;\n\n@RestPackController\n@RequestMapping(\"/demo/restpack/error\")\npublic class RestPackErrorController {\n\n    private static final Logger log = LoggerFactory.getLogger(RestPackErrorController.class);\n\n    /**\n     * 方法内部抛出的 BusinessException 异常。\n     * http://localhost:8080/demo/restpack/error/be?msg=abc\n     */\n    @RequestMapping(value = \"/be\", method = RequestMethod.GET)\n    public void toError(@RequestParam(value = \"msg\") String msg) throws BusinessException {\n        log.info(\"error, msg = {}\", msg);\n        throw new BusinessException(\"invalid.msg\")\n                .setMessage(\"消息格式无效: ${msg}\")\n                .put(\"msg\", msg);\n    }\n\n    /**\n     * 方法内部抛出的 RuntimeException 异常。\n     * http://localhost:8080/demo/restpack/error/re?msg=abc\n     */\n    @RequestMapping(value = \"/re\", method = RequestMethod.GET)\n    public void toError2(@RequestParam(value = \"msg\") String msg) throws Exception {\n        log.info(\"error, msg = {}\", msg);\n        throw new RuntimeException(\"error msg.\");\n    }\n\n    /**\n     * 参数类型是基本类型（这意味着不允许为null），但这个参数却没传。\n     * http://localhost:8080/demo/restpack/error/be/pin\n     */\n    @RequestMapping(value = \"/be/pin\", method = RequestMethod.GET)\n    public void toErrorByPrimitiveIsNull(long id) {\n        log.info(\"error, id = {}\", id);\n    }\n\n\n\n}\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/test/restpack/HttpResultConverterTest.java",
    "content": "package com.terran4j.test.restpack;\n\nimport com.terran4j.commons.restpack.EnableRestPack;\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.restpack.HttpResultConverter;\nimport com.terran4j.commons.restpack.impl.HttpResultMapper;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\n@SpringBootTest(\n        classes = {HttpResultConverterTest.RestPackApp.class}\n)\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class HttpResultConverterTest {\n\n    public static class MyHttpResult {\n\n        private Object data;\n\n        private String code;\n\n        private String msg;\n\n        public Object getData() {\n            return data;\n        }\n\n        public void setData(Object data) {\n            this.data = data;\n        }\n\n        public String getCode() {\n            return code;\n        }\n\n        public void setCode(String code) {\n            this.code = code;\n        }\n\n        public String getMsg() {\n            return msg;\n        }\n\n        public void setMsg(String msg) {\n            this.msg = msg;\n        }\n    }\n\n    public static class MyHttpResultConverter implements HttpResultConverter {\n\n        @Override\n        public Object convert(HttpResult httpResult) {\n            MyHttpResult myResult = new MyHttpResult();\n            myResult.setCode(httpResult.getResultCode());\n            myResult.setData(httpResult.getData());\n            myResult.setMsg(httpResult.getMessage());\n            return myResult;\n        }\n    }\n\n    @EnableRestPack\n    @SpringBootApplication\n    public static class RestPackApp {\n\n        @Bean\n        public HttpResultConverter httpResultConverter() {\n            return new MyHttpResultConverter();\n        }\n    }\n\n    @Autowired\n    private HttpResultMapper httpResultMapper;\n\n    @Test\n    public void testConvertWithSuccess() throws Exception {\n        Object data = new String[]{\"pig\", \"dog\", \"cat\"};\n        HttpResult httpResult = HttpResult.successFully(data);\n        Object result = httpResultMapper.convert(httpResult);\n        if (result instanceof MyHttpResult) {\n            MyHttpResult myHttpResult = (MyHttpResult) result;\n            Assert.assertEquals(data, myHttpResult.getData());\n        } else {\n            Assert.fail(\"Object result should be MyHttpResult, but is: \"\n                    + result.getClass().getName());\n        }\n    }\n}\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/test/restpack/HttpResultMapperTest.java",
    "content": "package com.terran4j.test.restpack;\n\nimport com.terran4j.commons.restpack.EnableRestPack;\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.restpack.impl.HttpResultMapper;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n@SpringBootTest(\n        classes = {HttpResultMapperTest.RestPackApp.class}\n)\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class HttpResultMapperTest {\n\n    private static final Logger log = LoggerFactory.getLogger(HttpResultMapperTest.class);\n\n    @EnableRestPack\n    @SpringBootApplication\n    public static class RestPackApp {\n    }\n\n    @Autowired\n    private HttpResultMapper httpResultMapper;\n\n    @Test\n    public void testToMapWithSuccess() throws Exception {\n        log.info(\"testToMapWithSuccess\");\n\n        Object data = new String[]{\"pig\", \"dog\", \"cat\"};\n        HttpResult httpResult = HttpResult.successFully(data);\n        Map<String, Object> map = httpResultMapper.toMap(httpResult);\n\n        Assert.assertNotNull(map.get(\"requestCode\"));\n        Assert.assertNull(map.get(\"requestId\"));\n        Assert.assertNotNull(map.get(\"currentTime\"));\n        Assert.assertNull(map.get(\"serverTime\"));\n        Assert.assertNotNull(map.get(\"spend\"));\n        Assert.assertNull(map.get(\"spendTime\"));\n\n        Assert.assertEquals(data, map.get(\"result\"));\n        Assert.assertNull(map.get(\"data\"));\n        Assert.assertEquals(\"OK\", map.get(\"status\"));\n        Assert.assertNull(map.get(\"resultCode\"));\n    }\n\n    @Test\n    public void testToMapWithFailure() throws Exception {\n        log.info(\"testToMapWithFailure\");\n\n        String msg = \"key is null\";\n        BusinessException be = new BusinessException(ErrorCodes.NULL_PARAM)\n                .setMessage(msg).put(\"key\", \"k1\");\n        HttpResult httpResult = HttpResult.fail(be);\n        Map<String, Object> map = httpResultMapper.toMap(httpResult);\n\n        Assert.assertEquals(ErrorCodes.NULL_PARAM, map.get(\"status\"));\n        Assert.assertNull(map.get(\"resultCode\"));\n\n        Assert.assertEquals(msg, map.get(\"msg\"));\n        Assert.assertNull(map.get(\"message\"));\n\n        Map<String, Object> props = new HashMap<>();\n        props.put(\"key\", \"k1\");\n        Assert.assertEquals(props, map.get(\"data\"));\n        Assert.assertNull(map.get(\"props\"));\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/test/restpack/NoHttpResultConverterTest.java",
    "content": "package com.terran4j.test.restpack;\n\nimport com.terran4j.commons.restpack.EnableRestPack;\nimport com.terran4j.commons.restpack.HttpResult;\nimport com.terran4j.commons.restpack.impl.HttpResultMapper;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport java.util.Map;\n\n@SpringBootTest(\n        classes = {NoHttpResultConverterTest.RestPackApp.class}\n)\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class NoHttpResultConverterTest {\n\n    @EnableRestPack\n    @SpringBootApplication\n    public static class RestPackApp {\n    }\n\n    @Autowired\n    private HttpResultMapper httpResultMapper;\n\n    @Test\n    public void testConvertWithSuccess() throws Exception {\n        Object data = new String[]{\"pig\", \"dog\", \"cat\"};\n        HttpResult httpResult = HttpResult.successFully(data);\n        Object result = httpResultMapper.convert(httpResult);\n        if (result instanceof Map) {\n            Map<String, Object> myHttpResult = (Map<String, Object>) result;\n            Assert.assertEquals(data, myHttpResult.get(\"result\"));\n        } else {\n            Assert.fail(\"Object result should be Map<String, Object>, but is: \"\n                    + result.getClass().getName());\n        }\n    }\n}\n"
  },
  {
    "path": "commons-restpack/src/test/java/com/terran4j/test/restpack/RestPackTest.java",
    "content": "package com.terran4j.test.restpack;\n\nimport com.terran4j.commons.restpack.RestPackIgnore;\nimport com.terran4j.commons.restpack.impl.RestPackUtils;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n@Slf4j\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class RestPackTest {\n\n    public static class WrapTypeBean {\n\n        @RestPackIgnore\n        private Boolean value = true;\n\n        public WrapTypeBean() {\n        }\n\n        public WrapTypeBean(Boolean value) {\n            this.value = value;\n        }\n\n        public Boolean getValue() {\n            return value;\n        }\n\n        public void setValue(Boolean value) {\n            this.value = value;\n        }\n\n    }\n\n    @Test\n    public void testClearIgnoreFieldOfWrapType() throws Exception {\n        log.info(\"testClearIgnoreFieldOfWrapType\");\n        WrapTypeBean bean = new WrapTypeBean();\n        Assert.assertTrue(bean.getValue());\n        RestPackUtils.clearIgnoreFields(bean);\n        Assert.assertNull(bean.getValue());\n    }\n\n    public static class BasicTypeBean {\n\n        @RestPackIgnore\n        private boolean value = true;\n\n        public boolean getValue() {\n            return value;\n        }\n\n        public void setValue(boolean value) {\n            this.value = value;\n        }\n\n    }\n\n    @Test\n    public void testClearIgnoreFieldOfBasicType() throws Exception {\n        log.info(\"testClearIgnoreFieldOfBasicType\");\n        BasicTypeBean bean = new BasicTypeBean();\n        Assert.assertTrue(bean.getValue());\n        try {\n            RestPackUtils.clearIgnoreFields(bean);\n            Assert.fail(\"应该出错！\");\n        } catch (Exception e) {\n            Assert.assertTrue(e.getMessage().contains(\"不允许修饰在基本类型字段上\"));\n        }\n    }\n\n\n    public static class ListBean {\n\n        @RestPackIgnore\n        private Integer value = 1;\n\n        private List<WrapTypeBean> beans = new ArrayList<>();\n\n        public Integer getValue() {\n            return value;\n        }\n\n        public void setValue(Integer value) {\n            this.value = value;\n        }\n\n        public List<WrapTypeBean> getBeans() {\n            return beans;\n        }\n\n        public void setBeans(List<WrapTypeBean> beans) {\n            this.beans = beans;\n        }\n    }\n\n    @Test\n    public void testClearIgnoreFieldOfList() throws Exception {\n        log.info(\"testClearIgnoreFieldOfList begin.\");\n        ListBean bean = new ListBean();\n        bean.getBeans().add(new WrapTypeBean(true));\n        bean.getBeans().add(new WrapTypeBean(false));\n        RestPackUtils.clearIgnoreFields(bean);\n        Assert.assertNull(bean.getValue());\n        Assert.assertEquals(2, bean.getBeans().size());\n        Assert.assertNull(bean.getBeans().get(0).getValue());\n        Assert.assertNull(bean.getBeans().get(1).getValue());\n        log.info(\"testClearIgnoreFieldOfList end.\");\n    }\n\n    public static class MapBean {\n\n        @RestPackIgnore\n        private Integer value = 1;\n\n        private Map<String, WrapTypeBean> beans = new HashMap<>();\n\n        public Integer getValue() {\n            return value;\n        }\n\n        public void setValue(Integer value) {\n            this.value = value;\n        }\n\n        public Map<String, WrapTypeBean> getBeans() {\n            return beans;\n        }\n\n        public void setBeans(Map<String, WrapTypeBean> beans) {\n            this.beans = beans;\n        }\n    }\n\n    @Test\n    public void testClearIgnoreFieldOfMap() throws Exception {\n        log.info(\"testClearIgnoreFieldOfMap begin.\");\n        MapBean bean = new MapBean();\n        bean.getBeans().put(\"a\", new WrapTypeBean(true));\n        bean.getBeans().put(\"b\", new WrapTypeBean(false));\n        RestPackUtils.clearIgnoreFields(bean);\n        Assert.assertNull(bean.getValue());\n        Assert.assertEquals(2, bean.getBeans().size());\n        Assert.assertNull(bean.getBeans().get(\"a\").getValue());\n        Assert.assertNull(bean.getBeans().get(\"b\").getValue());\n        log.info(\"testClearIgnoreFieldOfMap end.\");\n    }\n\n}\n"
  },
  {
    "path": "commons-restpack/src/test/resources/application.yml",
    "content": "\nterran4j:\n  restpack:\n    renaming:\n      requestId: requestCode\n      serverTime: currentTime\n      spendTime: spend\n      resultCode: status\n      data: result\n      message: msg\n      props: data\n      success: OK\n\n"
  },
  {
    "path": "commons-restpack/src/test/resources/logback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration debug=\"false\" scan=\"true\" scanPeriod=\"1000\">\n\n    <appender name=\"stdout\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"restpackDebug\"\n              class=\"com.terran4j.commons.restpack.log.RestPackLogAppender\">\n        <!--<encoder>-->\n            <!--<pattern>%date %level requestId=%X{requestId} &#45;&#45; %-40logger{35}[%line]: %msg%n</pattern>-->\n        <!--</encoder>-->\n    </appender>\n\n    <!--<appender name=\"file\" class=\"ch.qos.logback.core.FileAppender\">-->\n    <!--<file>./restpack.log</file>-->\n    <!--<encoder>-->\n    <!--<pattern>%date %level requestId=%X{requestId} &#45;&#45; %-40logger{35}[%line]: %msg%n</pattern>-->\n    <!--</encoder>-->\n    <!--</appender>-->\n\n    <root level=\"info\">\n        <appender-ref ref=\"stdout\"/>\n        <appender-ref ref=\"restpackDebug\"/>\n    </root>\n\n</configuration>"
  },
  {
    "path": "commons-restpack/src/test/resources/static/restpack/hello.css",
    "content": "\n.hello {\n    font-size: xx-large;\n}"
  },
  {
    "path": "commons-restpack/src/test/resources/templates/restpack/hello.ftl",
    "content": "<html>\n<head>\n    <link rel=\"stylesheet\" href=\"hello.css\">\n</head>\n<body>\n<div class=\"hello\">\n    Hello ${name}.\n</div>\n<div>\n    Welcome to RestPack!\n</div>\n<div>\n    This is author of RestPack: <img src=\"me.jpeg\"/>\n</div>\n</body>\n</html>"
  },
  {
    "path": "commons-test/.gitignore",
    "content": "/target/\n/.settings/\n/.classpath\n/.project\nterran4j-commons-test.iml"
  },
  {
    "path": "commons-test/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.github.terran4j</groupId>\n        <artifactId>terran4j-commons-parent</artifactId>\n        <version>1.0.4-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>terran4j-commons-test</artifactId>\n    <packaging>jar</packaging>\n    <name>terran4j-commons-test</name>\n    <url>https://github.com/terran4j/commons</url>\n\n\n    <dependencies>\n        <!-- terran4j工具类库 -->\n        <dependency>\n            <groupId>com.github.terran4j</groupId>\n            <artifactId>terran4j-commons-util</artifactId>\n        </dependency>\n\n        <!-- 引入数据库支持，用于初始化测试数据。 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-jpa</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-jdbc</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>mysql</groupId>\n            <artifactId>mysql-connector-java</artifactId>\n        </dependency>\n\n        <!-- spring boot测试支持。 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-all</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/BaseHttpTest.java",
    "content": "//package com.terran4j.commons.test;\n//\n//import com.terran4j.commons.hi.HttpClient;\n//import com.terran4j.commons.hi.HttpException;\n//import com.terran4j.commons.hi.Request;\n//import com.terran4j.commons.hi.Session;\n//import org.junit.After;\n//import org.junit.Before;\n//import org.junit.Test;\n//import org.slf4j.Logger;\n//import org.slf4j.LoggerFactory;\n//import org.springframework.beans.factory.annotation.Autowired;\n//import org.springframework.context.ApplicationContext;\n//import org.springframework.util.StringUtils;\n//\n//public class BaseHttpTest extends BaseSpringBootTest {\n//\n//\tprivate static final Logger log = LoggerFactory.getLogger(BaseHttpTest.class);\n//\n//\t@Autowired\n//\tprotected ApplicationContext context;\n//\n//\tprotected HttpClient httpClient = null;\n//\n//\tprotected Session session = null;\n//\n//\t@Before\n//\tpublic void setUp() throws Exception {\n//\t\thttpClient = HttpClient.create(context);\n//\t\tString appSecret = getAppSecret();\n//\t\tif (!StringUtils.isEmpty(appSecret)) {\n//\t\t\thttpClient.addListener(new SigListener(appSecret));\n//\t\t}\n//\t\tif (log.isInfoEnabled()) {\n//\t\t\tlog.info(\"create httpClient.\");\n//\t\t}\n//\t}\n//\n//\t@After\n//\tpublic void tearDown() throws Exception {\n//\t\tthis.session = null;\n//\t}\n//\n//\t@Test\n//\tpublic void testEmpty() throws Exception {\n//\t}\n//\n//\tprotected final Request request(String action) throws HttpException {\n//\t\tif (this.session == null) {\n//\t\t\tthis.session = httpClient.create();\n//\t\t}\n//\t\treturn this.session.action(action);\n//\t}\n//\n//\tprotected final Request request(String action, boolean newSession) throws HttpException {\n//\t\tif (this.session == null || newSession) {\n//\t\t\tthis.session = httpClient.create();\n//\t\t}\n//\t\treturn this.session.action(action);\n//\t}\n//\n//\tprotected final Request login() throws HttpException {\n//\t\tthis.session = httpClient.create();\n//\t\treturn this.session.action(getLoginAction());\n//\t}\n//\n//\tprotected final Request login(String action) throws HttpException {\n//\t\tthis.session = httpClient.create();\n//\t\treturn this.session.action(action);\n//\t}\n//\n//\t/**\n//\t * 如果需要计算参数签名，就覆盖本方法返回 appSecret 。\n//\t *\n//\t * @return\n//\t */\n//\tprotected String getAppSecret() {\n//\t\treturn null;\n//\t}\n//\n//\t/**\n//\t * 登录请求的名称。\n//\t * @return\n//\t */\n//\tprotected String getLoginAction() {\n//\t\treturn \"login\";\n//\t}\n//\n//}\n"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/BaseSpringBootTest.java",
    "content": "package com.terran4j.commons.test;\n\nimport org.junit.runner.RunWith;\nimport org.springframework.test.context.TestExecutionListeners;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.test.context.support.DependencyInjectionTestExecutionListener;\n\n@RunWith(SpringJUnit4ClassRunner.class)\n@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class })\npublic abstract class BaseSpringBootTest {\n\n\n}\n"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/BaseTestExecutionListener.java",
    "content": "package com.terran4j.commons.test;\n\nimport com.terran4j.commons.util.Classes;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.TestContext;\nimport org.springframework.test.context.TestExecutionListener;\n\npublic class BaseTestExecutionListener implements TestExecutionListener {\n\n\t@Override\n\tpublic void beforeTestClass(TestContext testContext) throws Exception {\n\t}\n\n\t@Override\n\tpublic void prepareTestInstance(TestContext testContext) throws Exception {\n\t}\n\n\t@Override\n\tpublic void beforeTestMethod(TestContext testContext) throws Exception {\n\t}\n\n\t@Override\n\tpublic void afterTestMethod(TestContext testContext) throws Exception {\n\t}\n\n\t@Override\n\tpublic void afterTestClass(TestContext testContext) throws Exception {\n\t}\n\t\n\tprotected final Class<?>[] getSpringBootClasses(TestContext testContext) {\n\t\tClass<?> testClass = testContext.getTestClass();\n\t\tSpringBootTest testAnno = Classes.getAnnotation(testClass, SpringBootTest.class);\n\t\tif (testAnno == null) {\n\t\t\treturn new Class<?>[0];\n\t\t}\n\t\t\n\t\tClass<?>[] springBootClasses = testAnno.classes();\n\t\tif (springBootClasses != null && springBootClasses.length > 0) {\n\t\t\treturn springBootClasses;\n\t\t}\n\t\treturn new Class<?>[0];\n\t}\n\n}"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/DatabaseInitializer.java",
    "content": "package com.terran4j.commons.test;\n\nimport com.terran4j.commons.util.Classes;\nimport com.terran4j.commons.util.Strings;\nimport org.junit.Assert;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.test.context.TestContext;\nimport org.springframework.util.StringUtils;\n\nimport javax.persistence.Entity;\nimport java.io.IOException;\nimport java.lang.reflect.Method;\nimport java.util.HashSet;\nimport java.util.Set;\n\n/**\n * 执行TestCase方法前，对数据库数据进行初始化。<br>\n * <p>\n * 1. 根据 TestCase 类（或父类）上的 @TruncateTable 注解，将相关的表清空。<br>\n * 2. 如果 @SpringBootTest 类有同包同名的 SQL 文件，将执行一下此 SQL 文件。<br>\n * 3. 如果 TestCase 类有同包同名的 SQL 文件，将执行一下此 SQL 文件。<br>\n * </p>\n * \n * @author jiangwei\n *\n */\npublic class DatabaseInitializer extends BaseTestExecutionListener {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(DatabaseInitializer.class);\n\n\t@Autowired\n\tprotected JdbcTemplate jdbcTemplate;\n\n\t@Override\n\tpublic void beforeTestMethod(TestContext testContext) throws Exception {\n\t\tjdbcTemplate = testContext.getApplicationContext().getBean(JdbcTemplate.class);\n\n\t\t// 如果发现有JPA的实体类，先清空对应表中的所有数据。\n\t\tClass<?> testClass = testContext.getTestClass();\n\t\tTruncateTable truncateTable = Classes.getAnnotation(testClass, TruncateTable.class);\n\t\tif (truncateTable != null && truncateTable.basePackageClass() != null) {\n\t\t\tSet<Package> pkgs = new HashSet<>();\n\t\t\tfor (Class<?> basePackageClass : truncateTable.basePackageClass()) {\n\t\t\t\tPackage pkg = basePackageClass.getPackage();\n\t\t\t\tif (pkgs.contains(pkg)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttruncateTables(basePackageClass);\n\t\t\t\tpkgs.add(pkg);\n\t\t\t}\n\t\t}\n\n\t\t// 整个 app 测试用的全局 SQL 文件。\n\t\tClass<?>[] springBootClasses = getSpringBootClasses(testContext);\n\t\tfor (Class<?> springBootClass : springBootClasses) {\n\t\t\texeSQLFile(springBootClass);\n\t\t}\n\n\t\t// 本 TestCase 类单独用的 SQL 文件。\n\t\texeSQLFile(testClass);\n\n\t\t// 本 TestCase 方法单独用的 SQL 文件。\n\t\tMethod testMethod = testContext.getTestMethod();\n\t\tString methodSQLName = testClass.getSimpleName() + \".\" + testMethod.getName() + \".sql\";\n\t\texeSQLFile(testClass, methodSQLName);\n\t}\n\n\tvoid truncateTables(Class<?> springBootClass) throws ClassNotFoundException, IOException {\n\t\tSet<Class<?>> entityClasses = Classes.scanClasses(springBootClass, Entity.class);\n\t\tif (entityClasses != null && entityClasses.size() > 0) {\n\t\t\tStringBuilder sqls = new StringBuilder();\n\t\t\tsqls.append(\"SET NAMES utf8mb4;\\n\");\n\t\t\tsqls.append(\"SET FOREIGN_KEY_CHECKS = 0;\\n\");\n\t\t\tfor (Class<?> entityClass : entityClasses) {\n\t\t\t\tEntity entity = entityClass.getAnnotation(Entity.class);\n\t\t\t\tString tableName = entity.name();\n\t\t\t\tsqls.append(\"truncate table `\").append(tableName).append(\"`;\\n\");\n\t\t\t}\n\t\t\tsqls.append(\"SET FOREIGN_KEY_CHECKS = 1;\");\n\t\t\texeSQLs(sqls.toString());\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"execute truncate table sqls:\\n{}\", sqls);\n\t\t\t}\n\t\t}\n\t}\n\n\tfinal String removeSQLComments(String sql) {\n\t\tif (StringUtils.isEmpty(sql)) {\n\t\t\treturn sql;\n\t\t}\n\t\tint fromIndex = 0;\n\t\tint length = sql.length();\n\t\tStringBuffer sb = new StringBuffer();\n\t\twhile (fromIndex < length) {\n\t\t\tint startPos = sql.indexOf(\"--\", fromIndex);\n\t\t\tif (startPos < 0 || startPos >= length) {\n\t\t\t\tsb.append(sql.substring(fromIndex, length));\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\tsb.append(sql.substring(fromIndex, startPos));\n\t\t\t\tint endPos = sql.indexOf(\"\\n\", startPos);\n\t\t\t\tif (endPos < 0 || endPos >= length) {\n\t\t\t\t\tbreak;\n\t\t\t\t} else {\n\t\t\t\t\tfromIndex = endPos + 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn sb.toString().trim();\n\t}\n\n\tprotected void exeSQLs(String sqls) {\n\t\tif (StringUtils.isEmpty(sqls)) {\n\t\t\treturn;\n\t\t}\n\t\tString[] sqlArray = Strings.splitWithTrim(sqls, \";\");\n\t\tfor (String sql : sqlArray) {\n\t\t\tif (StringUtils.isEmpty(sql)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsql = removeSQLComments(sql);\n\t\t\tsql = sql.trim();\n\t\t\tif (StringUtils.isEmpty(sql)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\texeSQL(sql);\n\t\t}\n\t}\n\n\tprotected void exeSQL(String sql) {\n\t\tif (StringUtils.isEmpty(sql)) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"prepare execute sql: {}\", sql);\n\t\t\t}\n\t\t\tjdbcTemplate.execute(sql);\n\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\tlog.info(\"execute sql done: {}\", sql);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\tString msg = String.format(\"execute sql[%s] failed: %s\", sql, e.getMessage());\n\t\t\tlog.error(msg, e);\n\t\t\tAssert.fail(msg);\n\t\t}\n\t}\n\n\tvoid exeSQLFile(Class<?> nameAsClass) {\n\t\tString fileName = nameAsClass.getSimpleName() + \".sql\";\n\t\texeSQLFile(nameAsClass, fileName);\n\t}\n\n\tvoid exeSQLFile(Class<?> nameAsClass, String fileName) {\n\t\tString fileContent = Strings.getString(nameAsClass, fileName);\n\t\tif (StringUtils.isEmpty(fileContent)) {\n\t\t\tlog.warn(\"sql file not found or empty, package = {}, file = \", nameAsClass.getPackage().getName(),\n\t\t\t\t\tfileName);\n\t\t\treturn;\n\t\t}\n\t\texeSQLs(fileContent);\n\t}\n\n}\n"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/DatabaseTestConfig.java",
    "content": "package com.terran4j.commons.test;\n\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.PropertySource;\n\n\n@Configuration\n@PropertySource({\n        \"classpath:com/terran4j/commons/test/database.properties\",\n})\npublic class DatabaseTestConfig {\n\n}\n"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/ExtAssert.java",
    "content": "package com.terran4j.commons.test;\n\nimport com.terran4j.commons.util.DateTimes;\nimport org.junit.Assert;\n\nimport java.util.Date;\n\npublic class ExtAssert {\n\n\tprotected ExtAssert() {\n\t}\n\n\tstatic String formatClassAndValue(Object value, String valueString) {\n\t\tString className = value == null ? \"null\" : value.getClass().getName();\n\t\treturn className + \"<\" + valueString + \">\";\n\t}\n\n\tstatic String format(String message, Object expected, Object actual) {\n\t\tString formatted = \"\";\n\t\tif (message != null && !message.equals(\"\")) {\n\t\t\tformatted = message + \" \";\n\t\t}\n\t\tString expectedString = String.valueOf(expected);\n\t\tString actualString = String.valueOf(actual);\n\t\tif (expectedString.equals(actualString)) {\n\t\t\treturn formatted + \"expected: \" + formatClassAndValue(expected, expectedString) + \" but was: \"\n\t\t\t\t\t+ formatClassAndValue(actual, actualString);\n\t\t} else {\n\t\t\treturn formatted + \"expected:<\" + expectedString + \"> but was:<\" + actualString + \">\";\n\t\t}\n\t}\n\n\tstatic String format(Object expected, Object actual) {\n\t\treturn format(null, expected, actual);\n\t}\n\n\t/**\n\t * 比较两个日期对象，但允许一定的时间差。\n\t * \n\t * @param expected\n\t *            期望值。\n\t * @param actual\n\t *            实际值。\n\t * @param expectedDifferenceTime\n\t *            允许的时间差。\n\t */\n\tpublic static void assertEquals(Date expected, Date actual, long expectedDifferenceTime) {\n\t\tif (expected == null && actual == null) {\n\t\t\treturn;\n\t\t}\n\t\tif (expected == null || actual == null) {\n\t\t\tAssert.fail(format(expected, actual));\n\t\t}\n\t\tlong actualDifferenceTime = Math.abs(expected.getTime() - actual.getTime());\n\t\tif (actualDifferenceTime > expectedDifferenceTime) {\n\t\t\tString msg = String.format(\"%s, expected difference is no more than %d ms, but actual is %d ms\",\n\t\t\t\t\tformat(DateTimes.toString(expected), DateTimes.toString(actual)), expectedDifferenceTime,\n\t\t\t\t\tactualDifferenceTime);\n\t\t\tAssert.fail(msg);\n\t\t}\n\t}\n\t\n\tpublic static void waitFor(final long millis) {\n\t\tif (millis <= 0) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tlong expired = System.currentTimeMillis() + millis;\n\t\t\n\t\tlong step = 0;\n\t\tif (millis > 1000) {\n\t\t\tstep = 100;\n\t\t} else if (millis < 10){\n\t\t\tstep = 1;\n\t\t} else {\n\t\t\tstep = millis / 10;\n\t\t}\n\t\t\n\t\twhile(true) {\n\t\t\tif (System.currentTimeMillis() > expired) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tThread.sleep(step);\n\t\t\t} catch (InterruptedException e) {\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/MockitoInitializer.java",
    "content": "package com.terran4j.commons.test;\n\nimport org.mockito.MockitoAnnotations;\nimport org.springframework.test.context.TestContext;\n\npublic class MockitoInitializer extends BaseTestExecutionListener {\n\n\t@Override\n\tpublic void prepareTestInstance(TestContext testContext) throws Exception {\n\t\t// 初始化测试用例类中由Mockito的注解标注的所有模拟对象\n\t\tObject instance = testContext.getTestInstance();\n\t\tMockitoAnnotations.initMocks(instance);\n\t}\n}\n"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/RedisTestConfig.java",
    "content": "package com.terran4j.commons.test;\n\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.PropertySource;\n\n@Configuration\n@PropertySource(\"classpath:com/terran4j/commons/test/redis.properties\")\npublic class RedisTestConfig {\n\n}\n"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/SigListener.java",
    "content": "//package com.terran4j.commons.test;\n//\n//import com.terran4j.commons.hi.HttpClientListener;\n//import com.terran4j.commons.hi.HttpRequest;\n//import org.slf4j.Logger;\n//import org.slf4j.LoggerFactory;\n//import org.springframework.util.StringUtils;\n//\n//import java.security.MessageDigest;\n//import java.security.NoSuchAlgorithmException;\n//import java.util.Arrays;\n//import java.util.HashSet;\n//import java.util.Map;\n//import java.util.Set;\n//\n//public class SigListener implements HttpClientListener {\n//\n//\tprivate static final Logger log = LoggerFactory.getLogger(SigListener.class);\n//\n//\tprivate final String appSecret;\n//\n//\tprivate final Set<String> pathPrefixs = new HashSet<>();\n//\n//\tpublic SigListener(String appSecret) {\n//\t\tsuper();\n//\t\tthis.appSecret = appSecret;\n//\t}\n//\n//\tpublic SigListener addPathPrefix(String pathPrefix) {\n//\t\tif (StringUtils.isEmpty(pathPrefix)) {\n//\t\t\treturn this;\n//\t\t}\n//\n//\t\tpathPrefixs.add(pathPrefix);\n//\t\treturn this;\n//\t}\n//\n//\t@Override\n//\tpublic void beforeExecute(HttpRequest request) {\n//\t\tString url = request.getUrl();\n//\n//\t\tboolean matched = false;\n//\t\tif (pathPrefixs.size() > 0) {\n//\t\t\tfor (String pathPrefix : pathPrefixs) {\n//\t\t\t\tif (url.startsWith(pathPrefix)) {\n//\t\t\t\t\tmatched = true;\n//\t\t\t\t\tbreak;\n//\t\t\t\t}\n//\t\t\t}\n//\t\t} else {\n//\t\t\tmatched = true;\n//\t\t}\n//\t\tif (!matched) {\n//\t\t\treturn;\n//\t\t}\n//\n//\t\tMap<String, String> params = request.getParams();\n//\t\tif (params == null) {\n//\t\t\treturn;\n//\t\t}\n//\n//\t\tString sig = signature(params, appSecret);\n//\t\tparams.put(\"sig\", sig);\n//\t\tif (log.isInfoEnabled()) {\n//\t\t\tlog.info(\"do beforeExecute, sig = {}\", sig);\n//\t\t}\n//\t}\n//\n//\t@Override\n//\tpublic String afterExecute(HttpRequest request, String reponse) {\n//\t\treturn reponse;\n//\t}\n//\n//\t/**\n//     * 计算签名\n//     *\n//     * @param requestParams\n//     * @return\n//     */\n//    public static String signature(Map<String, String> requestParams, String appSecret) {\n//\n//        StringBuilder buffer = new StringBuilder();\n//\n//        Object[] keys = requestParams.keySet().toArray();\n//        Arrays.sort(keys);\n//        for (Object key : keys) {\n//            String value = requestParams.get(key);\n//            if (!\"sig\".equals(key) && !StringUtils.isEmpty(value)) {\n//                buffer.append(key + \"=\" + value + \"&\");\n//            }\n//        }\n//\n//        String sign = \"\";\n//        if (buffer.length() > 0) {\n//            sign += buffer.substring(0, buffer.length() - 1);\n//        }\n//\n//        sign += appSecret;\n//        return md5(sign);\n//    }\n//\n//    private static String md5(String input) {\n//        MessageDigest md = null;\n//        try {\n//            md = MessageDigest.getInstance(\"MD5\");\n//        } catch (NoSuchAlgorithmException e) {\n//        }\n//\n//        md.update(input.getBytes());\n//        byte byteData[] = md.digest();\n//        StringBuffer buffer = new StringBuffer();\n//        for (int i = 0; i < byteData.length; i++) {\n//            buffer.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1));\n//        }\n//        return buffer.toString();\n//    }\n//\n//}\n"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/TruncateTable.java",
    "content": "package com.terran4j.commons.test;\n\nimport java.lang.annotation.*;\n\n/**\n * 清空指定的表的数据。\n */\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\n@Inherited\npublic @interface TruncateTable {\n\n\t/**\n\t *\n\t * @return\n\t */\n\tClass<?>[] basePackageClass() default {};\n\n}"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/database.properties",
    "content": "\nspring.datasource.driverClassName = com.mysql.jdbc.Driver\nspring.datasource.url = jdbc:mysql://127.0.0.1:3306/test?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8\nspring.datasource.username = root\nspring.datasource.password = Asd123\nspring.jpa.hibernate.ddl-auto = create\nspring.jpa.show-sql = false\nspring.jpa.jackson.serialization.indent_output = true"
  },
  {
    "path": "commons-test/src/main/java/com/terran4j/commons/test/redis.properties",
    "content": "\nspring.redis.host = 127.0.0.1\nspring.redis.port = 6379\nspring.redis.pool.max-idle = 8\nspring.redis.pool.min-idle = 0\nspring.redis.pool.max-total = 8\nspring.redis.pool.max-wait = -1"
  },
  {
    "path": "commons-test/src/test/java/com/terran4j/commons/test/HelloService.java",
    "content": "package com.terran4j.commons.test;\n\npublic class HelloService {\n\t\n\tprivate volatile int count = 0;\n\t\n\tpublic String sayHello(String name) {\n\t\tcount++;\n\t\treturn \"Hello \" + name;\n\t}\n\n\tpublic int getCount() {\n\t\treturn count;\n\t}\n\n}"
  },
  {
    "path": "commons-test/src/test/java/com/terran4j/commons/test/MockitoTest.java",
    "content": "package com.terran4j.commons.test;\n\nimport org.junit.Test;\nimport org.springframework.test.context.TestExecutionListeners;\n\nimport static org.junit.Assert.assertEquals;\nimport static org.mockito.Mockito.*;\n\n@TestExecutionListeners({ MockitoInitializer.class })\npublic class MockitoTest {\n\n\t/**\n\t * mock方法可以基于一个接口生成一个模拟对象， 你可以指定这个模拟对象的行为，比如方法的返回值。\n\t */\n\t@Test\n\tpublic void testMock() {\n\t\t// 创建mock对象，参数可以是类，也可以是接口\n\t\tHelloService mock = mock(HelloService.class);\n\n\t\t// 设置方法的预期返回值\n\t\twhen(mock.sayHello(\"terran4j\")).thenReturn(\"Hello terran4j\");\n\t\twhen(mock.getCount()).thenReturn(10);\n\n\t\t// junit测试\n\t\tString result = mock.sayHello(\"terran4j\");\n\t\tassertEquals(\"Hello terran4j\", result);\n\t\tint count = mock.getCount();\n\t\tassertEquals(10, count);\n\n\t\t// 验证方法是否调用了sayHello(\"terran4j\")\n\t\tverify(mock).sayHello(\"terran4j\");\n\t}\n\n\t/**\n\t * spy方法可以模拟一个实际对象的部分行为（比如方法的返回值），但其它未模拟的行为不受影响。\n\t */\n\t@Test\n\tpublic void testSpy() {\n\n\t\t// 创建 spy 对象，参数可以是类，也可以是接口\n\t\tHelloService hello = new HelloService();\n\t\tHelloService spy = spy(hello);\n\n\t\t// 设置 getCount 方法的预期返回值\n\t\tdoReturn(100).when(spy).getCount();\n\t\t// 不设置 sayHello 方法的预期返回值 doReturn(\"Hello\n\t\t// abc\").when(spy).sayHello(\"abc\");\n\n\t\t// 未被 spy 模拟的方法不受影响。\n\t\tString result = spy.sayHello(\"terran4j\");\n\t\tassertEquals(\"Hello terran4j\", result);\n\t\tresult = hello.sayHello(\"terran4j\");\n\t\tassertEquals(\"Hello terran4j\", result);\n\n\t\t// 被 spy 模拟方法返回预设的值。\n\t\thello.sayHello(\"terran4j\");\n\t\thello.sayHello(\"terran4j\");\n\t\tassertEquals(3, hello.getCount());\n\t\tspy.sayHello(\"terran4j\");\n\t\tspy.sayHello(\"terran4j\");\n\t\tassertEquals(100, spy.getCount());\n\t}\n}\n"
  },
  {
    "path": "commons-test/src/test/java/com/terran4j/commons/test/TestApp.java",
    "content": "package com.terran4j.commons.test;\n\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class TestApp {\n\n}\n"
  },
  {
    "path": "commons-util/.gitignore",
    "content": "/target/\n/.settings/\n/.classpath\n/.project\nterran4j-commons-util.iml\n"
  },
  {
    "path": "commons-util/pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<parent>\n\t\t<groupId>com.github.terran4j</groupId>\n\t\t<artifactId>terran4j-commons-parent</artifactId>\n\t\t<version>1.0.4-SNAPSHOT</version>\n\t</parent>\n\n\t<artifactId>terran4j-commons-util</artifactId>\n\t<packaging>jar</packaging>\n\t<name>terran4j-commons-util</name>\n\t<url>https://github.com/terran4j/commons</url>\n\n\t<dependencies>\n\t\t<dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.gson</groupId>\n            <artifactId>gson</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-collections4</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>commons-beanutils</groupId>\n            <artifactId>commons-beanutils</artifactId>\n            <version>1.9.4</version>\n        </dependency>\n        <dependency>\n            <groupId>joda-time</groupId>\n            <artifactId>joda-time</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n\t\t\t<artifactId>bcprov-jdk15on</artifactId>\n\t\t</dependency>\n\t</dependencies>\n\n</project>\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Arrays.java",
    "content": "package com.terran4j.commons.util;\n\npublic class Arrays {\n\n\t/**\n\t * 将两个String数组合并成一个，第2个数组直接连在第1个数组的后面，不会重排序。\n\t * @param array1 第 1 个数组对象\n\t * @param array2 第 2 个数组对象\n\t * @return 合并后的数组对象\n\t */\n\tpublic static String[] concat(String[] array1, String[] array2) {\n\t\tif (array1 == null && array2 == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif (array1 == null) {\n\t\t\treturn safeCopy(array2);\n\t\t}\n\t\tif (array2 == null) {\n\t\t\treturn safeCopy(array1);\n\t\t}\n\n\t\tString[] dest = new String[array1.length + array2.length];\n\t\tSystem.arraycopy(array1, 0, dest, 0, array1.length);\n\t\tSystem.arraycopy(array2, 0, dest, array1.length,\n\t\t\t\tarray2.length);\n\t\treturn dest;\n\t}\n\n\tpublic static String[] safeCopy(String[] source) {\n\t\tif (source == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tString[] dest = new String[source.length];\n\t\tSystem.arraycopy(source, 0, dest, 0, source.length);\n\t\treturn dest;\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Beans.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.lang.reflect.Field;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Modifier;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.Set;\n\nimport org.apache.commons.beanutils.BeanUtils;\n\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\nimport com.terran4j.commons.util.value.ValueSource;\n\n/**\n * 处理属性只含基本类型的javabean。\n * @author wei.jiang\n */\npublic class Beans {\n\n\tprivate static final Set<String> baseTypes = new HashSet<String>();\n\n\tstatic {\n\t\tbaseTypes.add(\"boolean\");\n\t\tbaseTypes.add(\"byte\");\n\t\tbaseTypes.add(\"char\");\n\t\tbaseTypes.add(\"short\");\n\t\tbaseTypes.add(\"int\");\n\t\tbaseTypes.add(\"long\");\n\t\tbaseTypes.add(\"float\");\n\t\tbaseTypes.add(\"double\");\n\t}\n\n\tprivate static List<Class<?>> baseClasses = new ArrayList<Class<?>>();\n\n\tstatic {\n\t\tbaseClasses.add(Boolean.class);\n\t\tbaseClasses.add(Byte.class);\n\t\tbaseClasses.add(Character.class);\n\t\tbaseClasses.add(Short.class);\n\t\tbaseClasses.add(Integer.class);\n\t\tbaseClasses.add(Long.class);\n\t\tbaseClasses.add(Float.class);\n\t\tbaseClasses.add(Double.class);\n\t\tbaseClasses.add(String.class);\n\t}\n\t\n\tprivate static Object toBaseObject(Class<?> clazz, String value) {\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tObject paramObj = null;\n\t\t// TODO: 应该支持所有基本类型，及像Date这样的常用类型，这里还不完善，有待补充。\n\t\tif (clazz.equals(Integer.class) || clazz.getName() == \"int\") {\n\t\t\tparamObj = Integer.valueOf(value);\n\t\t} else if (clazz.equals(Double.class) || clazz.getName() == \"double\") {\n\t\t\tparamObj = Double.valueOf(value);\n\t\t} else if (clazz.equals(Long.class) || clazz.getName() == \"long\") {\n\t\t\tparamObj = Long.valueOf(value);\n\t\t} else if (clazz.equals(Boolean.class) || clazz.getName() == \"boolean\") {\n\t\t\tparamObj = Boolean.valueOf(value);\n\t\t} else {\n\t\t\tparamObj = value;\n\t\t}\n\n\t\treturn paramObj;\n\t}\n\n\tprivate static String upcaseHeadLetter(String str) {\n\t\treturn str.substring(0, 1).toUpperCase() + str.substring(1);\n\t}\n\t\n\tprivate static boolean contains(int value, int flag) {\n\t\treturn (value & flag) == flag;\n\t}\n\t\n\tprivate static boolean isUpperLetter(char c) {\n\t\treturn c >= 'A' && c <= 'Z';\n\t}\n\t\n\tpublic static <T> T from(final ValueSource<String, Object> values, Class<T> clazz) {\n\t\ttry {\n\t\t\tT bean = clazz.newInstance();\n\t\t\tif (values == null) {\n\t\t\t\treturn bean;\n\t\t\t}\n\t\t\t\n\t\t\tMap<String, Method> setMethods = getAllSetMethods(clazz);\n\t\t\tif (setMethods == null || setMethods.size() == 0) {\n\t\t\t\treturn bean;\n\t\t\t}\n\t\t\t\n\t\t\tIterator<String> it = setMethods.keySet().iterator();\n\t\t\twhile (it.hasNext()) {\n\t\t\t\tString fieldName = it.next();\n\t\t\t\tMethod setMethod = setMethods.get(fieldName);\n\t\t\t\tObject value = values.get(fieldName);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\ttry {\n\t\t\t\t\tClass<?> parameterClass = setMethod.getParameterTypes()[0];\n\t\t\t\t\tObject paramObj = toBaseObject(parameterClass, value.toString());\n\t\t\t\t\tsetMethod.invoke(bean, paramObj);\n\t\t\t\t} catch (SecurityException | IllegalArgumentException | InvocationTargetException e) {\n\t\t\t\t\tString msg = String.format(\"set field[{}] of class[{}] to value[{}] failed: {}\", \n\t\t\t\t\t\t\tfieldName, clazz.getName(), value, e.getMessage());\n\t\t\t\t\tthrow new RuntimeException(msg, e);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn bean;\n\t\t} catch (InstantiationException | IllegalAccessException e) {\n\t\t\tString msg = String.format(\"create object[{}] from values[{}] failed: {}\", \n\t\t\t\t\tclazz.getName(), values, e.getMessage());\n\t\t\tthrow new RuntimeException(msg, e);\n\t\t}\n\t}\n\n\t/**\n\t * 根据 Properties 中的属性值，给 javabean 对象自动赋值。\n\t * @param <T> JaveBean 的类型。\n\t * @param props 属性\n\t * @param clazz javabean的类。\n\t * @return JaveBean 对象。\n\t */\n\tpublic static <T> T from(Properties props, Class<T> clazz) {\n\t\ttry {\n\t\t\tT bean = clazz.newInstance();\n\t\t\t\n\t\t\tif (props == null) {\n\t\t\t\treturn bean;\n\t\t\t}\n\t\t\t\n\t\t\tfor (Object keyObj : props.keySet()) {\n\t\t\t\tif (keyObj instanceof String) {\n\t\t\t\t\tString fieldName = (String) keyObj;\n\t\t\t\t\t\n\t\t\t\t\tObject value = props.get(fieldName);\n\t\t\t\t\tif (value == null) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tMethod setMethod = getSetMethod(clazz, fieldName);\n\t\t\t\t\tif (setMethod == null) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\ttry {\n\t\t\t\t\t\tClass<?> parameterClass = setMethod.getParameterTypes()[0];\n\t\t\t\t\t\tObject paramObj = toBaseObject(parameterClass, value.toString());\n\t\t\t\t\t\tsetMethod.invoke(bean, paramObj);\n\t\t\t\t\t} catch (SecurityException | IllegalArgumentException | InvocationTargetException e) {\n\t\t\t\t\t\tString msg = String.format(\"set field[{}] of class[{}] to value[{}] failed: {}\", \n\t\t\t\t\t\t\t\tfieldName, clazz.getName(), value, e.getMessage());\n\t\t\t\t\t\tthrow new RuntimeException(msg, e);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn bean;\n\t\t} catch (InstantiationException | IllegalAccessException e) {\n\t\t\tString msg = String.format(\"create object[{}] from properties[{}] failed: {}\", \n\t\t\t\t\tclazz.getName(), props, e.getMessage());\n\t\t\tthrow new RuntimeException(msg, e);\n\t\t}\n\t}\n\t\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T getFieldValue(Object bean, String fieldName, Class<?> filedClass) {\n\t\tif (bean == null) {\n\t\t\tthrow new NullPointerException(\"bean is null.\");\n\t\t}\n\t\tif (fieldName == null) {\n\t\t\tthrow new NullPointerException(\"fieldName is null.\");\n\t\t}\n\t\t\n\t\tClass<?> clazz = bean.getClass();\n\t\tField field = null;\n\t\ttry {\n\t\t\tfield = clazz.getDeclaredField(fieldName);\n\t\t} catch (SecurityException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t} catch (NoSuchFieldException e) {\n\t\t\treturn null;\n\t\t}\n\t\tif (field == null) {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\tObject fieldValue = null;\n\t\ttry {\n\t\t\tfieldValue = field.get(bean);\n\t\t} catch (IllegalArgumentException | IllegalAccessException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t\t\n\t\tif (fieldValue == null) {\n\t\t\treturn null;\n\t\t}\n\t\tif (!filedClass.isInstance(fieldValue)) {\n\t\t\tthrow new ClassCastException(\"field[\" + fieldName + \"] is not the Class: \" + filedClass);\n\t\t}\n\t\treturn (T) fieldValue;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static void setFieldValue(Object bean, String fieldName, Object value)\n            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {\n\t\tif (bean == null) {\n\t\t\tthrow new NullPointerException(\"bean is null.\");\n\t\t}\n\t\tif (fieldName == null) {\n\t\t\tthrow new NullPointerException(\"fieldName is null.\");\n\t\t}\n\n\t\tClass<?> clazz = bean.getClass();\n\t\tMethod setMethod = getSetMethod(clazz, fieldName);\n\t\tif (setMethod == null) {\n\t\t    String msg = String.format(\"field[%s] has not a setter method in class: %s\", fieldName, clazz);\n            throw new NoSuchMethodException(msg);\n        }\n\n        setMethod.invoke(bean, value);\n\t}\n\t\n\tpublic static Map<String, Method> getAllSetMethods(Class<?> clazz) {\n\t\tif (clazz == null) {\n\t\t\tthrow new NullPointerException(\"clazz is null.\");\n\t\t}\n\t\t\n\t\tMap<String, Method> result = new HashMap<String, Method>();\n\t\tMethod[] methods = clazz.getMethods();\n\t\tfor (Method method : methods) {\n\t\t\tString fieldName = getFieldNameBySetMethod(method);\n\t\t\tif (fieldName != null) {\n\t\t\t\tresult.put(fieldName, method);\n\t\t\t}\n\t\t}\n\t\t\n\t\treturn result;\n\t}\n\t\n\tpublic static String getFieldNameBySetMethod(Method method) {\n\t\tif (method == null) {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\tint modifiers = method.getModifiers();\n\t\tif (!contains(modifiers, Modifier.PUBLIC)) {\n\t\t\treturn null;\n\t\t}\n\t\tif (contains(modifiers, Modifier.STATIC)) {\n\t\t\treturn null;\n\t\t}\n\t\tif (method.isSynthetic()) {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\tClass<?>[] parameterTypes = method.getParameterTypes();\n\t\tif (parameterTypes == null || parameterTypes.length != 1) {\n\t\t\treturn null;\n\t\t}\n\t\tClass<?> parameterType = parameterTypes[0];\n\t\tif (!isBasicType(parameterType)) {\n\t\t\treturn null;\n\t\t}\n\t\tboolean isBooleanType = parameterType.equals(Boolean.class) || parameterType.getName() == \"boolean\";\n\t\t\n\t\tString name = method.getName();\n\t\tString fieldName = null;\n\t\tif (name.startsWith(\"is\") && isBooleanType) {\n\t\t\tif (name.length() < 3 || !isUpperLetter(name.charAt(2))) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tfieldName = name.substring(2, 3).toLowerCase() + name.substring(3);\n\t\t} else if (name.startsWith(\"set\") && !isBooleanType) {\n\t\t\tif (name.length() < 4 || !isUpperLetter(name.charAt(3))) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tfieldName = name.substring(3, 4).toLowerCase() + name.substring(4);\n\t\t}\n\t\t\n\t\treturn fieldName;\n\t}\n\n\tpublic static Method getSetMethod(Class<?> clazz, String fieldName) {\n\t\tString setMethodName = getSetMethodName(clazz, fieldName);\n\t\tMethod[] methods = clazz.getMethods();\n\t\tfor (Method method : methods) {\n\t\t\tif (method.getName().equals(setMethodName)) {\n\t\t\t\treturn method;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\tpublic static Method getGetMethod(Class<?> clazz, String fieldName) {\n\t\tString getMethodName = getGetMethodName(clazz, fieldName);\n\t\tMethod[] methods = clazz.getMethods();\n\t\tfor (Method method : methods) {\n\t\t\tif (method.getName().equals(getMethodName)) {\n\t\t\t\treturn method;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\tpublic static String getSetMethodName(Class<?> clazz, String fieldName) {\n\t\tif (isBooleanField(clazz, fieldName)) {\n\t\t\t// 若是 is 打头就把 is 换成 set, 并且后面的首字母大写。\n\t\t\tif (fieldName.startsWith(\"is\") && fieldName.length() > 2) {\n\t\t\t\tString fieldName2 = fieldName.substring(2);\n\t\t\t\treturn \"set\" + upcaseHeadLetter(fieldName2);\n\t\t\t}\n\t\t}\n\t\treturn \"set\" + upcaseHeadLetter(fieldName);\n\t}\n\n\tprivate static boolean isBooleanField(Class<?> clazz, String fieldName) {\n\t\tField field = null;\n\t\ttry {\n\t\t\tfield = clazz.getDeclaredField(fieldName);\n\t\t} catch (SecurityException e) {\n\t\t\te.printStackTrace();\n\t\t} catch (NoSuchFieldException e) {\n\t\t\te.printStackTrace();\n\t\t}\n\t\tif (field == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tString fieldClassName = field.getType().getName();\n\t\tif (\"boolean\".equals(fieldClassName) || Boolean.class.getName().equals(fieldClassName)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tpublic static String getGetMethodName(Class<?> clazz, String fieldName) {\n\t\tif (isBooleanField(clazz, fieldName)) {\n\t\t\tif (fieldName.startsWith(\"is\") && fieldName.length() > 2) {\n\t\t\t\t// 若是 is 打头就返回与 field 一样的名字。\n\t\t\t\treturn fieldName;\n\t\t\t} else {\n\t\t\t\treturn \"is\" + upcaseHeadLetter(fieldName);\n\t\t\t}\n\t\t}\n\t\treturn \"get\" + upcaseHeadLetter(fieldName);\n\t}\n\n\tpublic static boolean isBasicType(Class<?> clazz) {\n\t\tif (clazz == null) {\n\t\t\treturn false;\n\t\t}\n\t\tif (baseClasses.contains(clazz)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn baseTypes.contains(clazz.getName());\n\t}\n\t\n\t/**\n\t * \n\t * @param dest 目标对象。\n\t * @param orig 源对象。\n\t * @throws BusinessException 拷贝出错。\n\t */\n\tpublic static void copy(Object dest, Object orig) throws BusinessException {\n\t\tif (orig == null) {\n\t\t\treturn;\n\t\t}\n\t\tif (dest == null) {\n\t\t\tthrow new NullPointerException(\"dest is null.\");\n\t\t}\n\t\ttry {\n\t\t\tBeanUtils.copyProperties(dest, orig);\n\t\t} catch (IllegalAccessException | InvocationTargetException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t.setMessage(\"copy orig bean to dest bean error.\");\n\t\t}\n\t}\n\t\n\tpublic static <T> T createBy(Class<T> destClass, Object orig) throws BusinessException {\n\t\ttry {\n\t\t\tT dest = destClass.newInstance();\n\t\t\tcopy(dest, orig);\n\t\t\treturn dest;\n\t\t} catch (InstantiationException | IllegalAccessException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t.setMessage(\"create a new bean by orig bean error.\");\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Checker.java",
    "content": "package com.terran4j.commons.util;\n\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\nimport com.terran4j.commons.util.error.ErrorCodes;\n\npublic class Checker {\n\n\tpublic static final void checkNotNull(String value, String key) throws BusinessException {\n\t\tif (StringUtils.isEmpty(value)) {\n\t\t\tthrow new BusinessException(ErrorCodes.NULL_PARAM)\n\t\t\t\t\t.put(CommonErrorCode.KEY, key);\n\t\t}\n\t}\n\t\n\tpublic static final String checkLength(String value, int maxLenth, String key) throws BusinessException {\n\t\tif (StringUtils.isEmpty(value)) {\n\t\t\treturn value;\n\t\t}\n\t\tvalue = value.trim();\n\t\tif (value.length() > maxLenth) {\n\t\t\tthrow new BusinessException(ErrorCodes.INVALID_PARAM)\n\t\t\t\t\t.put(CommonErrorCode.KEY, key)\n\t\t\t\t\t.put(CommonErrorCode.MAX_LENGTH, maxLenth)\n\t\t\t\t\t.put(CommonErrorCode.ACUTAL_LENGTH, value.length())\n\t\t\t\t\t.setMessage(\"参数${key}值太长了\");\n\t\t}\n\t\treturn value;\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Classes.java",
    "content": "package com.terran4j.commons.util;\n\nimport com.google.common.base.Joiner;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.aop.TargetClassAware;\nimport org.springframework.core.io.Resource;\nimport org.springframework.core.io.support.PathMatchingResourcePatternResolver;\nimport org.springframework.core.io.support.ResourcePatternResolver;\nimport org.springframework.core.type.classreading.CachingMetadataReaderFactory;\nimport org.springframework.core.type.classreading.MetadataReader;\nimport org.springframework.core.type.classreading.MetadataReaderFactory;\nimport org.springframework.core.type.filter.AnnotationTypeFilter;\nimport org.springframework.core.type.filter.TypeFilter;\nimport org.springframework.util.Assert;\nimport org.springframework.util.ClassUtils;\nimport org.springframework.util.StringUtils;\n\nimport java.io.IOException;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.util.*;\n\npublic class Classes {\n\n    private static final Logger log = LoggerFactory.getLogger(Classes.class);\n\n    private static final Map<String, Class<?>> baseTypes = new HashMap<>();\n\n    static {\n        baseTypes.put(\"boolean\", Boolean.class);\n        baseTypes.put(\"byte\", Byte.class);\n        baseTypes.put(\"char\", Character.class);\n        baseTypes.put(\"short\", Short.class);\n        baseTypes.put(\"int\", Integer.class);\n        baseTypes.put(\"long\", Long.class);\n        baseTypes.put(\"float\", Float.class);\n        baseTypes.put(\"double\", Double.class);\n    }\n\n    /**\n     * 如果是基本类型，则转换成对应的包裹类型；<br>\n     * 如果不是基本类型，则原样返回。<br>\n     * 如：<br>\n     * 输入为 int, 返回 java.lang.Integer; <br>\n     * 输入为 java.lang.String, 返回 java.lang.String\n     *\n     * @param clazz 基本类对象。\n     * @return 包裹类对象。\n     */\n    public static Class<?> toWrapType(Class<?> clazz) {\n        if (clazz == null) {\n            return null;\n        }\n        if (clazz.isPrimitive()) {\n            return baseTypes.get(clazz.getName());\n        } else {\n            return clazz;\n        }\n    }\n\n    private static boolean isMatched(Class<?>[] paramClasses, Object[] paramObjects) {\n        if (paramClasses == null) {\n            paramClasses = new Class<?>[0];\n        }\n        if (paramObjects == null) {\n            paramObjects = new Object[0];\n        }\n\n        if (paramClasses.length != paramObjects.length) {\n            return false;\n        }\n\n        for (int i = 0; i < paramObjects.length; i++) {\n            Object arg = paramObjects[i];\n            if (arg == null) {\n                continue;\n            }\n            Class<?> argClass = arg.getClass();\n            Class<?> paramClass = paramClasses[i];\n            if (equals(argClass, paramClass)) {\n                continue;\n            }\n            if (isSuperClass(argClass, paramClass)) {\n                continue;\n            }\n            if (paramClass.isInterface() && isInterfaceExtends(argClass, paramClass)) {\n                continue;\n            }\n\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * 判断两个类是否相等，并且无视基本类型与包裹类型的差别，如：<br>\n     * srcClass = int; destClass = java.lang.Integer 也算是相等的。\n     *\n     * @param a 类对象 a 。\n     * @param b 类对象 b 。\n     * @return 是否相等。\n     */\n    public static boolean equals(Class<?> a, Class<?> b) {\n        if (a == null && b == null) {\n            return true;\n        }\n        if (a == null || b == null) {\n            return false;\n        }\n        a = toWrapType(a);\n        b = toWrapType(b);\n        return a == b;\n    }\n\n    /**\n     * 根据方法名、参数获取对应的方法对象，并且可以指定其上的注释进行过滤。\n     *\n     * @param clazz      类对象。\n     * @param methodName 方法\n     * @param args       参数值\n     * @param annoClass  注解类，如果为 null 表示不要求有注解，否则要求方法上一定要有这个注解。\n     * @return 方法对象。\n     */\n    public static Method getMethod(Class<?> clazz, String methodName, Object[] args,\n                                   Class<? extends Annotation> annoClass) {\n        Method[] methods = clazz.getMethods();\n        for (Method method : methods) {\n            if (!method.getName().equals(methodName)) {\n                continue;\n            }\n\n            Class<?>[] paramClasses = method.getParameterTypes();\n            if (!isMatched(paramClasses, args)) {\n                continue;\n            }\n\n            if (annoClass != null && method.getAnnotation(annoClass) == null) {\n                continue;\n            }\n\n            return method;\n        }\n        return null;\n    }\n\n    /**\n     * 判断一个类是否是另一个类的子孙类。<br>\n     * 如果两个类没有直接、间接继承关系，或相等，都会返回 false。\n     *\n     * @param child  子孙类\n     * @param parent 祖先类\n     * @return true 表示有直接或间接继承关系。\n     */\n    public static boolean isSuperClass(Class<?> child, Class<?> parent) {\n        if (child == null || parent == null) {\n            return false;\n        }\n\n        Class<?> currentSuperClass = child.getSuperclass();\n        while (currentSuperClass != null) {\n            if (currentSuperClass == parent) {\n                return true;\n            }\n            currentSuperClass = currentSuperClass.getSuperclass();\n        }\n\n        return false;\n    }\n\n    /**\n     * 判断一个类是否实现了指定接口，不向上追溯。\n     *\n     * @param clazz          类对象\n     * @param interfaceClass 接口类对象\n     * @return 判断结果\n     */\n    public static boolean isInterface(Class<?> clazz, Class<?> interfaceClass) {\n        if (clazz == null || interfaceClass == null) {\n            return false;\n        }\n\n        Class<?>[] interfaces = clazz.getInterfaces();\n        if (interfaceClass != null) {\n            for (Class<?> theInterfaceClass : interfaces) {\n                if (theInterfaceClass == interfaceClass) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * 判断一个类是否实现了指定接口，会向上追溯到父接口。\n     *\n     * @param clazz           类对象\n     * @param parentInterface 接口类对象\n     * @return 判断结果\n     */\n    public static final boolean isInterfaceExtends(Class<?> clazz, Class<?> parentInterface) {\n        if (parentInterface == null || !parentInterface.isInterface()) {\n            throw new NullPointerException(\"parentInterface is null or is NOT interface\");\n        }\n\n        if (parentInterface.equals(clazz)) {\n            return true;\n        }\n\n        // 向上遍历父接口。\n        Class<?>[] interfaces = clazz.getInterfaces();\n        for (Class<?> childClass : interfaces) {\n            if (isInterfaceExtends(childClass, parentInterface)) {\n                return true;\n            }\n        }\n\n        // 检查父类及其接口。\n        Class<?> supperClass = clazz.getSuperclass();\n        if (supperClass != null && isInterface(supperClass, parentInterface)) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * 对包及子包进行扫描，找出有指定注解的类。\n     *\n     * @param basePackageClass 待扫描的包中的直接任意类，也就是根据这个类找到它的包。\n     * @param annotationFilter 对注解过滤，只有上面有这个注解的类才会被找出来；如果为 null，则不过滤。\n     * @return 符合条件的类集合。\n     * @throws IOException            IO异常\n     * @throws ClassNotFoundException 类找不到。\n     */\n    public static final Set<Class<?>> scanClasses(Class<?> basePackageClass,\n                                                  Class<? extends Annotation> annotationFilter) throws IOException, ClassNotFoundException {\n        TypeFilter filter = annotationFilter == null ? null : new AnnotationTypeFilter(annotationFilter, false);\n        return scanClasses(basePackageClass, true, filter, Thread.currentThread().getContextClassLoader());\n    }\n\n    /**\n     * 扫描指定的包，将有指定注解的类找出来。\n     *\n     * @param basePackageClass 待扫描的包中的直接任意类，也就是根据这个类找到它的包。\n     * @param recursively      是否递归的方式在子包中找， true表示要在所有子包中找，false表示只在本包中找。\n     * @param filter           指定的过滤条件，只有符合过滤条件的类才会被找出来；如果为 null，则不过滤。\n     * @param classLoader      类加载器。\n     * @return 符合条件的类集合。\n     * @throws IOException            IO异常\n     * @throws ClassNotFoundException 类找不到。\n     */\n    public static final Set<Class<?>> scanClasses(Class<?> basePackageClass, boolean recursively, TypeFilter filter,\n                                                  ClassLoader classLoader) throws IOException, ClassNotFoundException {\n        if (basePackageClass == null) {\n            throw new NullPointerException(\"basePackageClass is null.\");\n        }\n\n        final Set<Class<?>> classes = new HashSet<Class<?>>();\n\n        String packageName = basePackageClass.getPackage().getName();\n        final String resourcePattern = recursively ? \"/**/*.class\" : \"/*.class\";\n        String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX\n                + ClassUtils.convertClassNameToResourcePath(packageName) + resourcePattern;\n        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();\n        MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);\n\n        final String KEY_maxCount = \"terran4j.util.maxScanClassCount\";\n        String maxCountText = System.getProperty(KEY_maxCount, \"1024\").trim();\n        int maxCount = 1024;\n        try {\n            maxCount = Integer.parseInt(maxCountText);\n        } catch (NumberFormatException e) {\n            throw new RuntimeException(\"Can't parse as int of java property[\" + KEY_maxCount + \"]: \" + maxCountText);\n        }\n\n        Resource[] resources = resourcePatternResolver.getResources(pattern);\n        for (Resource resource : resources) {\n            if (resource.isReadable()) {\n                // 找到这个类。\n                MetadataReader reader = readerFactory.getMetadataReader(resource);\n                String className = reader.getClassMetadata().getClassName();\n\n                boolean matched = false;\n                if (filter != null) {\n                    matched = filter.match(reader, readerFactory);\n                } else {\n                    matched = true;\n                }\n\n                if (matched) {\n                    Class<?> clazz = classLoader.loadClass(className);\n                    classes.add(clazz);\n\n                    // 调用方指定的范围太大的话，\n                    if (classes.size() > maxCount) {\n                        throw new RuntimeException(\"too many classes be scaned, can't more than: \" + maxCount);\n                    }\n                }\n            }\n        }\n\n        // 输出日志\n        if (log.isInfoEnabled()) {\n            if (filter == null) {\n                log.info(\"Found classes in package[{}]: \\n{}\", packageName, Joiner.on(\"\\n\").join(classes.iterator()));\n            } else {\n                log.info(\"Found classes with filter[{}] in package[{}]: \\n{}\", filter, packageName,\n                        Joiner.on(\"\\n\").join(classes.iterator()));\n            }\n\n        }\n        return classes;\n    }\n\n    /**\n     * 搜索匹配路径的搜索。\n     *\n     * @param pathPattern 路径匹配模式。\n     * @return 找到的资源文件。\n     * @throws IOException IO异常。\n     */\n    public static final Resource[] scanResources(String pathPattern) throws IOException {\n        String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + pathPattern;\n        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();\n        Resource[] resources = resolver.getResources(pattern);\n        return resources;\n    }\n\n    /**\n     * 获取一个对象的类<br>\n     * 在Spring AOP的拦截下，一些对象可能是由Cglib技术生成的代理类所创建，直接调用其getClass()方法获取到的类是这样的：<br>\n     * com.terran4j.XxxService$$EnhancerBySpringCGLIB$$da7b00df <br>\n     * 其实原始的类应该是： com.terran4j.XxxService。<br>\n     * 但由于Cglib的代理技术导致多了后面的 $$EnhancerBySpringCGLIB$$da7b00df。<br>\n     * 本方法可以自动识别对象是否是Cglib代理对象，如果是则找到它的原始类。\n     *\n     * @param object 对象（可能是代理对象）\n     * @return 原始类对象\n     */\n    public static Class<?> getTargetClass(Object object) {\n        Assert.notNull(object, \"Object must not be null\");\n        Class<?> result = null;\n        if (object instanceof TargetClassAware) {\n            result = ((TargetClassAware) object).getTargetClass();\n        }\n        if (result == null) {\n            result = (ClassUtils.isCglibProxy(object) ? object.getClass().getSuperclass() : object.getClass());\n        }\n        return result;\n    }\n\n    /**\n     * 根据字段名，找到指定字段。<br>\n     * 如果本类找不到，会向上在父类中找，直到找到为止。<br>\n     *\n     * @param name  字段名称。\n     * @param clazz 类对象。\n     * @return 字段对象。\n     */\n    public static Field getField(String name, Class<?> clazz) {\n        if (StringUtils.isEmpty(name) || clazz == null) {\n            return null;\n        }\n\n        Field[] fields = clazz.getDeclaredFields();\n        if (fields == null) {\n            return null;\n        }\n\n        for (Field field : fields) {\n            String fieldName = field.getName();\n            if (fieldName != null && fieldName.equals(name)) {\n                return field;\n            }\n        }\n\n        return getField(name, clazz.getSuperclass());\n    }\n\n    /**\n     * 根据注解，找到指定的字段。\n     *\n     * @param annotationClass 条件注解。\n     * @param clazz           目标类对象\n     * @return 满足条件注解的字段。\n     */\n    public static Field getField(Class<? extends Annotation> annotationClass, Class<?> clazz) {\n        if (clazz == null) {\n            return null;\n        }\n\n        Field[] fields = clazz.getDeclaredFields();\n        if (fields == null) {\n            return null;\n        }\n\n        for (Field field : fields) {\n            Annotation annotation = field.getAnnotation(annotationClass);\n            if (annotation != null) {\n                return field;\n            }\n        }\n\n        return getField(annotationClass, clazz.getSuperclass());\n    }\n\n    public static Field[] getFields(Class<? extends Annotation> annotationClass, final Class<?> clazz) {\n        List<Field> fieldList = new ArrayList<Field>();\n\n        // 将本类及所有的父类压栈。\n        Stack<Class<?>> classStack = new Stack<Class<?>>();\n        Class<?> currentClass = clazz;\n        while (currentClass != null) {\n            classStack.push(currentClass);\n            currentClass = currentClass.getSuperclass();\n        }\n\n        // 将栈中的类一个个取出来，加载注解的属性。\n        while (!classStack.isEmpty()) {\n            currentClass = classStack.pop();\n            loadFields(annotationClass, currentClass, fieldList);\n        }\n\n        return fieldList.toArray(new Field[fieldList.size()]);\n    }\n\n    /**\n     * 加载本类中拥有指定注解的域，范围只限于本类。\n     *\n     * @param annotationClass 条件注解。\n     * @param clazz           目标类对象。\n     * @param fieldList       找到的字段放在本列表中。\n     */\n    private static void loadFields(Class<? extends Annotation> annotationClass, Class<?> clazz, List<Field> fieldList) {\n        Field[] fields = clazz.getDeclaredFields();\n        if (fields == null) {\n            return;\n        }\n\n        for (Field field : fields) {\n            Annotation annotation = field.getAnnotation(annotationClass);\n            if (annotation != null) {\n                fieldList.add(field);\n            }\n        }\n    }\n\n    public static <T extends Annotation> T getAnnotation(Class<?> clazz,\n                                                         Class<T> annoClass) {\n        T anno = clazz.getAnnotation(annoClass);\n        if (anno != null) {\n            return anno;\n        }\n        Class<?> superClass = clazz.getSuperclass();\n        if (superClass == null) {\n            return null;\n        }\n        return getAnnotation(superClass, annoClass);\n    }\n\n    public static Method getMethod(Class<? extends Annotation> annotationClass, Class<?> clazz) {\n\n        Method[] methods = clazz.getDeclaredMethods();\n        if (methods == null) {\n            return null;\n        }\n\n        for (Method method : methods) {\n            Annotation annotation = method.getAnnotation(annotationClass);\n            if (annotation != null) {\n                return method;\n            }\n        }\n\n        return null;\n    }\n\n    public static Method[] getMethods(Class<? extends Annotation> annotationClass, Class<?> clazz) {\n        List<Method> methodList = new ArrayList<Method>();\n\n        Method[] methods = clazz.getDeclaredMethods();\n        if (methods == null) {\n            return null;\n        }\n\n        for (Method method : methods) {\n            Annotation annotation = method.getAnnotation(annotationClass);\n            if (annotation != null) {\n                methodList.add(method);\n            }\n        }\n\n        return methodList.toArray(new Method[methodList.size()]);\n    }\n\n    public static final String toIdentify(Method method) {\n        StringBuffer sb = new StringBuffer();\n        sb.append(method.getDeclaringClass().getName()).append(\"::\").append(method.getName()).append(\"(\");\n        Class<?>[] paramTypes = method.getParameterTypes();\n        if (paramTypes != null && paramTypes.length > 0) {\n            String[] paramTypeNames = new String[paramTypes.length];\n            for (int i = 0; i < paramTypes.length; i++) {\n                paramTypeNames[i] = paramTypes[i].getName();\n            }\n            String paramsText = Joiner.on(\",\").join(paramTypeNames);\n            sb.append(paramsText);\n        }\n        sb.append(\")\");\n        return sb.toString();\n    }\n\n    public static boolean equals(Method m1, Method m2) {\n        if (m1 == null || m2 == null) {\n            return false;\n        }\n        if (!m1.getName().equals(m2.getName())) {\n            return false;\n        }\n        Class<?>[] m1t = m1.getParameterTypes();\n        Class<?>[] m2t = m2.getParameterTypes();\n        if (m1t == null && m2t == null) {\n            return true;\n        }\n        if (m1t == null || m2t == null || m1t.length != m2t.length) {\n            return false;\n        }\n        for (int i = 0; i < m1t.length; i++) {\n            if (!m1t[i].equals(m2t[i])) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/DateTimes.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.util.Calendar;\nimport java.util.Date;\n\nimport org.joda.time.LocalDate;\nimport org.joda.time.LocalDateTime;\nimport org.joda.time.format.DateTimeFormat;\nimport org.joda.time.format.DateTimeFormatter;\n\npublic class DateTimes {\n\t\n\tpublic static final String FORMAT_yyyyMMdd = \"yyyyMMdd\";\n\t\n\tpublic static final String FORMAT_yyyy_MM_dd = \"yyyy-MM-dd\";\n\t\n\tpublic static final String FORMAT_yyyy_MM_dd_HH_mm_ss = \"yyyy-MM-dd HH:mm:ss\";\n\t\n\tpublic static final String FORMAT_CHINESE = \"yyyy年MM月dd日 HH时mm分ss秒\";\n\t\n\tpublic static Date toDateTime(String dateText, String format) {\n\t\tDateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(format);\n\t\tLocalDateTime localDateTime = LocalDateTime.parse(dateText, dateTimeFormatter);\n\t\treturn localDateTime.toDate();\n\t}\n\t\n\tpublic static String toString(Date date) {\n\t\treturn toString(date, FORMAT_yyyy_MM_dd_HH_mm_ss);\n\t}\n\n\tpublic static String toString(Date date, String format) {\n\t\tDateTimeFormatter formatter = DateTimeFormat.forPattern(format);\n\t\treturn formatter.print(date.getTime());\n\t}\n\t\n\tpublic static Date toDate(String dateText) {\n\t\treturn toDate(dateText, FORMAT_yyyy_MM_dd_HH_mm_ss);\n\t}\n\n\tpublic static Date toDate(String dateText, String format) {\n\t\tDateTimeFormatter formatter = DateTimeFormat.forPattern(format);\n\t\tLocalDate localDate = LocalDate.parse(dateText, formatter);\n\t\treturn localDate.toDate();\n\t}\n\n\tpublic static String toDate(Date date, String format) {\n\t\treturn new LocalDate(date).toString(format);\n\t}\n\n\tpublic static Date yesterday() {\n\t\tCalendar cal = Calendar.getInstance();\n\t\tcal.setTime(new Date());\n\t\tcal.add(Calendar.DAY_OF_MONTH, -1);\n\t\treturn cal.getTime();\n\t}\n\t\n\tpublic static Date shiftDay(int day) {\n\t\tCalendar cal = Calendar.getInstance();\n\t\tcal.setTime(new Date());\n\t\tcal.add(Calendar.DAY_OF_MONTH, day);\n\t\treturn cal.getTime();\n\t}\n\t\n\tpublic static Date shiftMonth(int month) {\n\t\tCalendar cal = Calendar.getInstance();\n\t\tcal.setTime(new Date());\n\t\tcal.add(Calendar.MONTH, month);\n\t\treturn cal.getTime();\n\t}\n\n\tpublic static Date cutHour(Date date) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTime(date);\n        cal.set(Calendar.HOUR_OF_DAY, 0);\n        cal.set(Calendar.MINUTE, 0);\n        cal.set(Calendar.SECOND, 0);\n        cal.set(Calendar.MILLISECOND, 0);\n        return cal.getTime();\n    }\n\n    public static Date cutDay(Date date) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTime(date);\n        cal.set(Calendar.DAY_OF_MONTH, 1);\n        cal.set(Calendar.HOUR_OF_DAY, 0);\n        cal.set(Calendar.MINUTE, 0);\n        cal.set(Calendar.SECOND, 0);\n        cal.set(Calendar.MILLISECOND, 0);\n        return cal.getTime();\n    }\n\n    public static Date cutMonth(Date date) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTime(date);\n        cal.set(Calendar.MONTH, 0);\n        cal.set(Calendar.DAY_OF_MONTH, 1);\n        cal.set(Calendar.HOUR_OF_DAY, 0);\n        cal.set(Calendar.MINUTE, 0);\n        cal.set(Calendar.SECOND, 0);\n        cal.set(Calendar.MILLISECOND, 0);\n        return cal.getTime();\n    }\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Encoding.java",
    "content": "package com.terran4j.commons.util;\n\npublic enum Encoding {\n\n\tUTF8(\"UTF-8\"), GBK(\"GBK\");\n\t\n\tprivate String name;\n\t\n\tprivate Encoding(String name){\n\t\tthis.name = name;\n\t}\n\t\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\t\n\tpublic static Encoding getDefaultEncoding(){\n\t\treturn UTF8;\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Enums.java",
    "content": "package com.terran4j.commons.util;\n\nimport org.springframework.util.StringUtils;\n\nimport java.lang.reflect.Array;\nimport java.lang.reflect.Method;\n\npublic class Enums {\n\n    public static Object getEnumObject(Class<?> enumType, String name) {\n        if (!enumType.isEnum()) {\n            return null;\n        }\n        if (StringUtils.isEmpty(name)) {\n            return null;\n        }\n        Object enumArray = null;\n        try {\n            Method method = enumType.getMethod(\"values\");\n            enumArray = method.invoke(null,new Object[0]);\n            if (enumArray == null || !enumArray.getClass().isArray()) {\n                return null;\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        int size = Array.getLength(enumArray);\n        if (size <= 0) {\n            return null;\n        }\n        for (int i = 0; i < size; i++) {\n            Enum enumObject = (Enum)Array.get(enumArray, i);\n            if (enumObject.name().equals(name)) {\n                return enumObject;\n            }\n        }\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Expressions.java",
    "content": "package com.terran4j.commons.util;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.expression.EvaluationContext;\nimport org.springframework.expression.Expression;\nimport org.springframework.expression.ExpressionParser;\nimport org.springframework.expression.spel.standard.SpelExpressionParser;\nimport org.springframework.expression.spel.support.StandardEvaluationContext;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class Expressions {\n\n    private static Logger log = LoggerFactory.getLogger(Expressions.class);\n\n    private static final ExpressionParser parser = new SpelExpressionParser();\n\n    private static final Map<String, Expression> cache = new ConcurrentHashMap<>();\n\n    public static final Expression getExpression(String expEL) {\n        Expression exp = cache.get(expEL);\n        if (exp != null) {\n            return exp;\n        }\n        synchronized(Expressions.class) {\n            exp = cache.get(expEL);\n            if (exp != null) {\n                return exp;\n            }\n            exp = parser.parseExpression(expEL);\n            if (log.isInfoEnabled()) {\n                log.info(\"parseExpression done: {}\", expEL);\n            }\n            cache.put(expEL, exp);\n            return exp;\n        }\n    }\n\n    public static final <T> T parse(String el, Map<String, Object> params, Class<T> resultType) {\n        Expression expression = getExpression(el);\n        T value = expression.getValue(buildContext(params), resultType);\n        return value;\n    }\n\n    public static final Object parse(String el, Map<String, Object> params) {\n        Expression expression = getExpression(el);\n        Object value = expression.getValue(buildContext(params));\n        return value;\n    }\n\n    private static final EvaluationContext buildContext(Map<String, Object> params) {\n        EvaluationContext context = new StandardEvaluationContext();\n        if (params != null) {\n            Iterator<String> it = params.keySet().iterator();\n            while (it.hasNext()) {\n                String key = it.next();\n                Object value = params.get(key);\n                context.setVariable(key, value);\n            }\n        }\n        return context;\n    }\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Files.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class Files {\n\n    private static final Logger log = LoggerFactory.getLogger(Files.class);\n    \n    public static String readFile(File file) {\n        InputStream input = null;\n        try {\n            input = new FileInputStream(file);\n            return Strings.getString(input);\n        } catch (Exception e) {\n            String msg = String.format(\"readFile failed, path = %s\", file);\n            throw new RuntimeException(msg, e);\n        }\n    }\n\n    public static void writeFile(String content, File file) {\n        if (content == null) {\n            throw new NullPointerException(\"writeFile, but content is null.\");\n        }\n        if (file == null) {\n            throw new NullPointerException(\"writeFile, but file is null.\");\n        }\n\n        if (!file.exists() | file.isDirectory()) {\n            try {\n                file.createNewFile();\n            } catch (IOException e) {\n                String msg = String.format(\"createNewFile faild, path = %s\", file);\n                throw new RuntimeException(msg, e);\n            }\n        }\n\n        InputStream input = null;\n        OutputStream output = null;\n        try {\n            input = Strings.toInputStream(content);\n            output = new FileOutputStream(file);\n            long fileSize = IOUtils.copy(input, output);\n            log.info(\"writeFile success, path = {}, size = {}\", file.getAbsolutePath(), fileSize);\n        } catch (Exception e) {\n            String msg = String.format(\"writeFile faild, path = %s, content:\\n%s\", file, content);\n            throw new RuntimeException(msg, e);\n        } finally {\n            if (input != null) {\n                try {\n                    input.close();\n                } catch (IOException e) {\n                    // ignore.\n                }\n            }\n            if (output != null) {\n                try {\n                    output.close();\n                } catch (IOException e) {\n                    // ignore.\n                }\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/IOUtils.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.io.*;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\n\n/**\n * \n * @author wei.jiang\n */\npublic class IOUtils {\n\n\tprivate final static int DEFAULT_BUFFERSIZE = 1024 * 4;\n\n\tprivate final static int DEFAULT_SLEEP_COUNT = 3;\n\n\tpublic static InputStream getInputStream(Class<?> clazz, String fileName) {\n        String path = getClassPath(clazz, fileName);\n        InputStream in = clazz.getClassLoader().getResourceAsStream(path);\n        return in;\n    }\n\n    private static String getClassPath(final Class<?> clazz, String fileName) {\n        Package classPackage = clazz.getPackage();\n        if (classPackage != null) {\n            return classPackage.getName().replace('.', '/') + \"/\" + fileName;\n        } else {\n            return fileName;\n        }\n    }\n\n\tpublic static long copy(InputStream input, OutputStream output) throws IOException {\n        // 接口空校验，解决抛出异常，引发异常处理逻辑再次抛出异常而告警的问题\n        long count = 0;\n        if (input == null || output == null) {\n            return count;\n        }\n        byte[] buffer = new byte[DEFAULT_BUFFERSIZE];\n        int n = 0;\n        while (true) {\n            int read = input.read(buffer);\n            if (read < 0) {\n                break;\n            }\n            output.write(buffer, 0, read);\n            count += read;\n            output.flush();\n            n++;\n            if (n % DEFAULT_SLEEP_COUNT == 0) {\n                try {\n                    Thread.sleep(0);\n                } catch (InterruptedException e) {\n                    // ignore\n                }\n            }\n        }\n        return count;\n    }\n\t\n\tpublic static final byte[] getByteArray(InputStream in) {\n        ByteArrayOutputStream out = null;\n        try {\n            out = new ByteArrayOutputStream();\n            copy(in, out);\n            byte[] b = out.toByteArray();\n            return b;\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage(), e);\n        } finally {\n            if (out != null) {\n                try {\n                    out.close();\n                } catch (IOException e) {\n                }\n            }\n        }\n    }\n\n    public static String getFileContent(File file){\n        if(file.exists() == false)return \"\";\n        try{\n            byte[] bytes = Files.readAllBytes(file.toPath());\n            return new String(bytes, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            throw new RuntimeException(e);\n        } catch (IOException e) {\n            e.printStackTrace();\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void setFileContent(File file, String content){\n        Writer fstream = null;\n        BufferedWriter out = null;\n        try{\n            fstream = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);\n            out = new BufferedWriter(fstream);\n            out.write(content);\n        } catch (IOException e) {\n            e.printStackTrace();\n            throw new RuntimeException(e);\n        } finally {\n            if(out != null){\n                try {\n                    out.close();\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n            if(fstream != null){\n                try {\n                    fstream.close();\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n\n        }\n    }\n\n\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/IdWorker.java",
    "content": "package com.terran4j.commons.util;\n\n/**\n * Twitter_Snowflake<br>\n * SnowFlake的结构如下(每部分用-分开):<br>\n * 0-0000000000 0000000000 0000000000 0000000000 0-00000-00000-000000000000 <br>\n * 1位标识，由于long基本类型在Java中是带符号的，最高位是符号位，正数是0，负数是1，所以id一般是正数，最高位是0<br>\n * 41位时间截(毫秒级)，注意，41位时间截不是存储当前时间的时间截，而是存储时间截的差值（当前时间截 - 开始时间截)\n * 得到的值），这里的的开始时间截，一般是我们的id生成器开始使用的时间，由我们程序来指定的\n * （如下下面程序IdWorker类的startTime属性）。41位的时间截，可以使用69年，年T\n * = (1L &amp;&amp; 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>\n * 10位的数据机器位，可以部署在1024个节点，包括5位datacenterId和5位workerId<br>\n * 12位序列，毫秒内的计数，12位的计数顺序号支持每个节点每毫秒(同一机器，同一时间截)产生4096个ID序号<br>\n * 加起来刚好64位，为一个Long型。<br>\n * SnowFlake的优点是，整体上按照时间自增排序，并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分)，\n * 并且效率较高，经测试，SnowFlake每秒能够产生26万ID左右。\n */\npublic class IdWorker {\n\n\t// ==============================Fields===========================================\n\t/** 开始时间截 (2015-01-01) */\n\tprivate final long twepoch = 1420041600000L;\n\n\t/** 机器id所占的位数 */\n\tprivate final long workerIdBits = 5L;\n\n\t/** 数据标识id所占的位数 */\n\tprivate final long datacenterIdBits = 5L;\n\n\t/** 支持的最大机器id，结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */\n\tprivate final long maxWorkerId = -1L ^ (-1L << workerIdBits);\n\n\t/** 支持的最大数据标识id，结果是31 */\n\tprivate final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);\n\n\t/** 序列在id中占的位数 */\n\tprivate final long sequenceBits = 12L;\n\n\t/** 机器ID向左移12位 */\n\tprivate final long workerIdShift = sequenceBits;\n\n\t/** 数据标识id向左移17位(12+5) */\n\tprivate final long datacenterIdShift = sequenceBits + workerIdBits;\n\n\t/** 时间截向左移22位(5+5+12) */\n\tprivate final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;\n\n\t/** 生成序列的掩码，这里为4095 (0b111111111111=0xfff=4095) */\n\tprivate final long sequenceMask = -1L ^ (-1L << sequenceBits);\n\n\t/** 工作机器ID(0~31) */\n\tprivate long workerId;\n\n\t/** 数据中心ID(0~31) */\n\tprivate long datacenterId;\n\n\t/** 毫秒内序列(0~4095) */\n\tprivate long sequence = 0L;\n\n\t/** 上次生成ID的时间截 */\n\tprivate long lastTimestamp = -1L;\n\n\t// ==============================Constructors=====================================\n\t/**\n\t * 构造函数\n\t * \n\t * @param workerId\n\t *            工作ID (0~31)\n\t * @param datacenterId\n\t *            数据中心ID (0~31)\n\t */\n\tpublic IdWorker(long workerId, long datacenterId) {\n\t\tif (workerId > maxWorkerId || workerId < 0) {\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\tString.format(\"worker Id can't be greater than %d or less than 0\", maxWorkerId));\n\t\t}\n\t\tif (datacenterId > maxDatacenterId || datacenterId < 0) {\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\tString.format(\"datacenter Id can't be greater than %d or less than 0\", maxDatacenterId));\n\t\t}\n\t\tthis.workerId = workerId;\n\t\tthis.datacenterId = datacenterId;\n\t}\n\n\t// ==============================Methods==========================================\n\t/**\n\t * 获得下一个ID (该方法是线程安全的)\n\t * \n\t * @return SnowflakeId\n\t */\n\tpublic synchronized long nextId() {\n\t\tlong timestamp = timeGen();\n\n\t\t// 如果当前时间小于上一次ID生成的时间戳，说明系统时钟回退过这个时候应当抛出异常\n\t\tif (timestamp < lastTimestamp) {\n\t\t\tthrow new RuntimeException(String.format(\n\t\t\t\t\t\"Clock moved backwards.  Refusing to generate id for %d milliseconds\", lastTimestamp - timestamp));\n\t\t}\n\n\t\t// 如果是同一时间生成的，则进行毫秒内序列\n\t\tif (lastTimestamp == timestamp) {\n\t\t\tsequence = (sequence + 1) & sequenceMask;\n\t\t\t// 毫秒内序列溢出\n\t\t\tif (sequence == 0) {\n\t\t\t\t// 阻塞到下一个毫秒,获得新的时间戳\n\t\t\t\ttimestamp = tilNextMillis(lastTimestamp);\n\t\t\t}\n\t\t}\n\t\t// 时间戳改变，毫秒内序列重置\n\t\telse {\n\t\t\tsequence = 0L;\n\t\t}\n\n\t\t// 上次生成ID的时间截\n\t\tlastTimestamp = timestamp;\n\n\t\t// 移位并通过或运算拼到一起组成64位的ID\n\t\treturn ((timestamp - twepoch) << timestampLeftShift) //\n\t\t\t\t| (datacenterId << datacenterIdShift) //\n\t\t\t\t| (workerId << workerIdShift) //\n\t\t\t\t| sequence;\n\t}\n\n\t/**\n\t * 阻塞到下一个毫秒，直到获得新的时间戳\n\t * \n\t * @param lastTimestamp\n\t *            上次生成ID的时间截\n\t * @return 当前时间戳\n\t */\n\tprotected long tilNextMillis(long lastTimestamp) {\n\t\tlong timestamp = timeGen();\n\t\twhile (timestamp <= lastTimestamp) {\n\t\t\ttimestamp = timeGen();\n\t\t}\n\t\treturn timestamp;\n\t}\n\n\t/**\n\t * 返回以毫秒为单位的当前时间\n\t * \n\t * @return 当前时间(毫秒)\n\t */\n\tprotected long timeGen() {\n\t\treturn System.currentTimeMillis();\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Jsons.java",
    "content": "package com.terran4j.commons.util;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.PropertyNamingStrategy;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.google.gson.*;\nimport com.terran4j.commons.util.config.ConfigElement;\nimport org.apache.commons.lang3.StringUtils;\n\nimport java.io.*;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.text.SimpleDateFormat;\nimport java.util.*;\n\npublic class Jsons {\n\n    private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();\n\n    private static final com.google.gson.JsonParser parser = new com.google.gson.JsonParser();\n\n    private static final ObjectMapper objectMapper = createObjectMapper();\n\n    public static final ObjectMapper createObjectMapper() {\n        ObjectMapper objectMapper = new ObjectMapper();\n\n        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);\n\n        // 属性为空时（包括 null, 空串，空集合，空对象），不参与序列化。\n        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);\n\n        // Date 对象在序列化时，格式为 yyyy-MM-dd HH:mm:ss 。\n        objectMapper.setDateFormat(new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\"));\n\n        objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);\n        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);\n        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);\n        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);\n\n        // json串以良好的格式输出。\n        objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);\n\n        // 当属性为空或有问题时不参与序列化。\n        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);\n\n        // 未知的属性不参与反序列化。\n        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\n\n        return objectMapper;\n    }\n\n    public static final ObjectMapper getObjectMapper() {\n        return objectMapper;\n    }\n\n    public static final JsonElement parseJson(String jsonText) {\n        return parser.parse(jsonText);\n    }\n\n    /**\n     * 将xml转化为json\n     *\n     * @param element XML对象\n     * @return JSON对象\n     */\n    public static final JsonElement toJson(ConfigElement element) {\n        if (element == null) {\n            return null;\n        }\n\n        JsonObject json = new JsonObject();\n\n        json.add(\"tag\", new JsonPrimitive(element.getName()));\n\n        String value = element.getValue();\n        if (value != null) {\n            json.add(\"value\", new JsonPrimitive(value));\n        }\n\n        Set<String> attr = element.attrSet();\n        if (attr != null && attr.size() > 0) {\n            JsonObject jsonAttr = new JsonObject();\n            Iterator<String> it = attr.iterator();\n            while (it.hasNext()) {\n                String attrKey = it.next();\n                String attrValue = element.attr(attrKey);\n                if (attrKey != null && attrValue != null) {\n                    jsonAttr.add(attrKey, new JsonPrimitive(attrValue));\n                }\n            }\n            json.add(\"attr\", jsonAttr);\n        }\n\n        ConfigElement[] children = element.getChildren();\n        if (children != null && children.length > 0) {\n            JsonArray jsonChildren = new JsonArray();\n            for (ConfigElement child : children) {\n                JsonElement jsonChild = toJson(child);\n                if (jsonChild != null) {\n                    jsonChildren.add(jsonChild);\n                }\n            }\n            json.add(\"children\", jsonChildren);\n        }\n\n        return json;\n    }\n\n    public static String format(JsonElement json) {\n        String prettyJsonText = gson.toJson(json);\n        return prettyJsonText;\n    }\n\n    /**\n     * 格式化\n     *\n     * @param uglyJsonText 未格式化的 json 串。\n     * @return 格式化的 json 串。\n     */\n    public static String format(String uglyJsonText) {\n        JsonElement je = parser.parse(uglyJsonText);\n        String prettyJsonText = format(je);\n        return prettyJsonText;\n    }\n\n    public static Map<String, Object> toMap(JsonObject json) {\n        if (json == null) {\n            return null;\n        }\n\n        Map<String, Object> result = new HashMap<>();\n        Iterator<Map.Entry<String, JsonElement>> it = json.entrySet().iterator();\n        while (it.hasNext()) {\n            Map.Entry<String, JsonElement> entry = it.next();\n            String key = entry.getKey();\n            JsonElement element = entry.getValue();\n            Object value = toObject(element);\n            result.put(key, value);\n        }\n        return result;\n    }\n\n    public static String toJsonText(Object obj) throws JsonProcessingException {\n        if (obj == null) {\n            return null;\n        }\n\n        return objectMapper.writeValueAsString(obj);\n    }\n\n    public static <T> T toObject(String jsonText, Class<T> clazz) throws JsonProcessingException {\n        if (StringUtils.isBlank(jsonText)) {\n            return null;\n        }\n        return objectMapper.readValue(jsonText, clazz);\n    }\n\n    public static Object toObject(JsonElement element) {\n        if (element == null || element.isJsonNull()) {\n            return null;\n        }\n\n        Object value = null;\n        if (element.isJsonPrimitive()) {\n            JsonPrimitive primitive = element.getAsJsonPrimitive();\n            if (primitive.isBoolean()) {\n                value = primitive.getAsBoolean();\n            } else if (primitive.isNumber()) {\n                value = primitive.getAsNumber();\n            } else if (primitive.isString()) {\n                value = primitive.getAsString();\n            }\n        } else if (element.isJsonArray()) {\n            List<Object> list = new ArrayList<>();\n            JsonArray jsonArray = element.getAsJsonArray();\n            for (int i = 0; i < jsonArray.size(); i++) {\n                JsonElement child = jsonArray.get(i);\n                list.add(toObject(child));\n            }\n            value = list;\n        } else if (element.isJsonObject()) {\n            JsonObject jsonObject = element.getAsJsonObject();\n            Map<String, Object> map = new HashMap<>();\n            Iterator<Map.Entry<String, JsonElement>> it = jsonObject.entrySet().iterator();\n            while (it.hasNext()) {\n                Map.Entry<String, JsonElement> entry = it.next();\n                String key = entry.getKey();\n                JsonElement child = entry.getValue();\n                map.put(key, toObject(child));\n            }\n            value = map;\n        }\n\n        return value;\n    }\n\n    public static <T> T readJson(Class<T> clazz, File json) throws IOException {\n        Gson g = new GsonBuilder().disableHtmlEscaping().create();\n        Reader reader = null;\n        try{\n            reader = new InputStreamReader(new FileInputStream(json), StandardCharsets.UTF_8);\n            return g.fromJson(reader, clazz);\n        }finally {\n            if(reader != null)reader.close();\n        }\n    }\n\n    public static <T> void writeJson(T object, File file) throws IOException {\n        Gson g = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();\n        String content = g.toJson(object);\n        IOUtils.setFileContent(file, content);\n    }\n\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Maths.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.security.InvalidParameterException;\n\npublic class Maths {\n\n\t/**\n\t * 在指定的范围区间内取值，<br>\n\t * 当小于最小值时，取最小值；当大于最大值时，取最大值；在这个范围区间内，取原值。\n\t * \n\t * @param value 原值。\n\t * @param min 最小值。\n\t * @param max 最大值。\n\t * @return 范围内的值。\n\t */\n\tpublic static final int limitIn(int value, Integer min, Integer max) {\n\t\tif (min != null && max != null && min > max) {\n\t\t\tString msg = String.format(\"min can't larger than max, min = %d, max = %d.\", min, max);\n\t\t\tthrow new InvalidParameterException(msg);\n\t\t}\n\t\tif (max != null && value > max) {\n\t\t\tvalue = max;\n\t\t}\n\t\tif (min != null && value < min) {\n\t\t\tvalue = min;\n\t\t}\n\t\treturn value;\n\t}\n\n\tpublic static final long limitIn(long value, Long min, Long max) {\n\t\tif (min != null && max != null && min > max) {\n\t\t\tString msg = String.format(\"min can't larger than max, min = %d, max = %d.\", min, max);\n\t\t\tthrow new InvalidParameterException(msg);\n\t\t}\n\t\tif (max != null && value > max) {\n\t\t\tvalue = max;\n\t\t}\n\t\tif (min != null && value < min) {\n\t\t\tvalue = min;\n\t\t}\n\t\treturn value;\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Objects.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\npublic class Objects {\n    public static boolean isSame(Object o1, Object o2){\n        if(o1 == null && o2 == null)return true;\n        if(o1 == null || o2 == null)return false;\n        return o1.equals(o2);\n\n    }\n    public static boolean isSame(Boolean o1, Boolean o2){\n        if(o1 == null && o2 == null)return true;\n        if(o1 == null || o2 == null)return false;\n        return o1.booleanValue() == o2.booleanValue();\n    }\n\n    public static Object getField(Object object, String field) {\n        try {\n            Class<?> clazz = object.getClass();\n            Method method = method = clazz.getMethod(\"get\"+Character.toUpperCase(field.charAt(0))+field.substring(1));\n            return method.invoke(object);\n        } catch (NoSuchMethodException e) {\n            throw new RuntimeException(e);\n        } catch (InvocationTargetException e) {\n            throw new RuntimeException(e);\n        } catch (IllegalAccessException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Randoms.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.util.Random;\n\npublic class Randoms {\n\n\tprivate static final char[] TOKEN_CHARS = new char[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',\n\t\t\t'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G',\n\t\t\t'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1',\n\t\t\t'2', '3', '4', '5', '6', '7', '8', '9' };\n\t\n\tprivate static final char[] TOKEN_NUMBERS = new char[] {  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };\n\n\tprivate static SecureRandom random = null;\n\t\n\tprivate static SecureRandom getRandom() {\n\t\tif (random != null) {\n\t\t\treturn random;\n\t\t}\n\t\ttry {\n\t\t\trandom = SecureRandom.getInstance(\"SHA1PRNG\");\n\t\t\trandom.setSeed(System.currentTimeMillis());\n\t\t\treturn random;\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\t\n\tpublic static final String createToken(final int count) {\n\t\treturn createToken(count, TOKEN_CHARS);\n\t}\n\t\n\tpublic static final String createNumberToken(final int count) {\n\t\treturn createToken(count, TOKEN_NUMBERS);\n\t}\n\t\n\tpublic static final String createToken(final int count, char[] scope) {\n\t\tStringBuffer sb = new StringBuffer();\n\t\tRandom random = getRandom();\n\t\tfor (int i = 0; i < count; i++) {\n\t\t\tchar c = scope[random.nextInt(scope.length)];\n\t\t\tsb.append(String.valueOf(c));\n\t\t}\n\t\tString token = sb.toString();\n\t\treturn token;\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Server.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.net.InetAddress;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class Server {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(Server.class);\n\t\n\tprivate static volatile Server v = null;\n\t\n\tprivate static final Server getInstance() {\n\t\tif (v != null) {\n\t\t\treturn v;\n\t\t}\n\t\tsynchronized (Server.class) {\n\t\t\tif (v != null) {\n\t\t\t\treturn v;\n\t\t\t}\n\t\t\tv = new Server();\n\t\t\treturn v;\n\t\t}\n\t}\n\n\tprivate String serverIP;\n\n\tprivate String serverName;\n\n\t/**\n\t * 注意： InetAddress.getLocalHost() 是耗时操作，因此使用懒加载方式。\n\t */\n\tprivate Server() {\n\t\tsuper();\n\t\t\n\t\tInetAddress address = null;\n\t\ttry {\n\t\t\taddress = InetAddress.getLocalHost();\n\t\t} catch (Exception e) {\n\t\t\tlog.error(\"GetLocalHost failed: \" + e.getMessage(), e);\n\t\t}\n\n\t\tif (address == null) {\n\t\t\tthis.serverName = null;\n\t\t\tthis.serverIP = null;\n\t\t} else {\n\t\t\tthis.serverName = address.getHostName();\n\t\t\tthis.serverIP = address.getHostAddress();\n\t\t}\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"本机名称是：{}, IP是： {}\", serverName, serverIP);\n\t\t}\n\t}\n\n\tpublic static final String getServerIP() {\n\t\treturn getInstance().serverIP;\n\t}\n\n\tpublic static final String getServerName() {\n\t\treturn getInstance().serverName;\n\t}\n\n}"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/Strings.java",
    "content": "package com.terran4j.commons.util;\n\nimport java.io.BufferedReader;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.PrintWriter;\nimport java.io.UnsupportedEncodingException;\nimport java.security.InvalidParameterException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.AntPathMatcher;\nimport org.springframework.util.PathMatcher;\nimport org.springframework.util.StringUtils;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.terran4j.commons.util.value.ValueSource;\n\n/**\n * 字符串操作工具类。\n * \n * @author wei.jiang\n */\npublic class Strings {\n    \n    private static final Logger log = LoggerFactory.getLogger(Strings.class);\n\n    /**\n     * 字节数据转字符串专用集合\n     */\n    private static final char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};\n\n    /**\n     * 获取对象的描述信息。\n     * \n     * @param value 具体对象。\n     * @return 对象的字符串信息(按json串的语法描述)。\n     */\n    public static final String toString(Object value) {\n        if (value == null) {\n            return \"\";\n        }\n        try {\n            return Jsons.getObjectMapper().writeValueAsString(value);\n        } catch (JsonProcessingException e) {\n            throw new IllegalStateException(e);\n        }\n    }\n\n    /**\n     * 按一个异常对象按成字符串的描述输出。\n     * \n     * @param t 异常对象\n     * @return 异常堆栈的文本内容。\n     */\n    public static String getString(Throwable t) {\n        if (t == null) {\n            return null;\n        }\n\n        ByteArrayOutputStream out = new ByteArrayOutputStream();\n        try {\n            PrintWriter pw = new PrintWriter(out);\n            t.printStackTrace(pw);\n            pw.flush();\n            String str = new String(out.toByteArray(), Encoding.UTF8.getName());\n            return str;\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            throw new RuntimeException(e);\n        } finally {\n            try {\n                out.close();\n            } catch (Exception e) {\n                // ignore the error.\n            }\n        }\n    }\n\n    /**\n     * 从classpath中读取文本资源文件。<br>\n     * 比如：包 com.terran4j.demo3 下有一个类： Hello.java 和一个 hi.txt 文件：<br>\n     * 那 getString(Hello.class, \"hi.txt\") 方法可以获取 hi.txt 文件的内容。\n     * \n     * @param clazz 与文本文件在相同包下的类。\n     * @param fileName 广西文件的名称。\n     * @return 文件中的字符串。\n     */\n    public static String getString(Class<?> clazz, String fileName) {\n        String path = null;\n        ClassLoader loader = null;\n        if (clazz == null) {\n            path = fileName;\n            loader = Strings.class.getClassLoader();\n        } else {\n            path = getClassPath(clazz, fileName);\n            loader = clazz.getClassLoader();\n        }\n\n        return getResourceByPath(path, loader);\n    }\n\n    public static final String getResourceByPath(String path, ClassLoader loader) {\n        InputStream in = loader.getResourceAsStream(path);\n        if (in == null) {\n            return null;\n        }\n\n        try {\n            String str = getString(in);\n            if (str == null) {\n                str = \"\";\n            }\n            return str;\n        } finally {\n            if (in != null) {\n                try {\n                    in.close();\n                } catch (IOException e) {\n                }\n            }\n        }\n    }\n\n    /**\n     * 将字符串转化成一个输入流对象。\n     * \n     * @param str 字符串\n     * @return 对应的输入流\n     */\n    public static InputStream toInputStream(String str) {\n        if (str == null) {\n            return null;\n        }\n\n        try {\n            InputStream in = new ByteArrayInputStream(str.getBytes(Encoding.UTF8.getName()));\n            return in;\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n\n    /**\n     * 从输入流中读取出所有内容，并按字符串返回。\n     * \n     * @param in 输入流\n     * @return 文本内容。\n     */\n    public static String getString(InputStream in) {\n        return getString(in, Encoding.getDefaultEncoding());\n    }\n\n    /**\n     * 从输入流中读取出所有内容，并按字符串返回内容，并且可以指定字符串编码方式。\n     * \n     * @param in 输入流\n     * @param encoding 字符串编码方式\n     * @return 字符串内容\n     */\n    public static String getString(InputStream in, Encoding encoding) {\n        /*\n         * To convert the InputStream to String we use the BufferedReader.readLine() method. We\n         * iterate until the BufferedReader return null which means there's no more data to read.\n         * Each line will appended to a StringBuilder and returned as String.\n         */\n        StringBuilder sb = new StringBuilder();\n        try {\n            if (encoding == null) {\n                encoding = Encoding.getDefaultEncoding();\n            }\n            InputStreamReader inr = new InputStreamReader(in, encoding.getName());\n            BufferedReader reader = new BufferedReader(inr);\n\n            String line = null;\n            while ((line = reader.readLine()) != null) {\n                sb.append(line + \"\\n\");\n            }\n        } catch (IOException e) {\n            e.printStackTrace();\n            throw new RuntimeException(e);\n        }\n        // #1005 去掉in.close()语句。\n\n        return sb.toString();\n\n    }\n\n    /**\n     * 根据类对象， 获取同包下的文件的资源路径。\n     * \n     * @param clazz 类对象\n     * @param fileName 与类同包下的文件名\n     * @return 文件中的字符串内容。\n     */\n    public static String getClassPath(final Class<?> clazz, String fileName) {\n        Package classPackage = clazz.getPackage();\n        if (classPackage != null) {\n            return classPackage.getName().replace('.', '/') + \"/\" + fileName;\n        } else {\n            return fileName;\n        }\n    }\n\n    /**\n     * 将一个带变量的文本内容（后续称之为“模板内容”），\n     * 按给定的变量值格式化为具体的文本内容。<br>\n     * 模板内容可以用 ${ 与 } 包裹起来作为变量。<br>\n     * 比如：<br>\n     * 模板内容为：str = \"尊敬的${name}您好，XXXXX\";<br>\n     * 变量值为： args = {\"name\": \"terran4j\"};<br>\n     * 调用 <code>format(str, args)</code> 方法后，返回的内容为：<br>\n     * 尊敬的terran4j您好，XXXXX\n     * \n     * @param str 模板内容。\n     * @param args 变量值。\n     * @param nullAsEmpty 如果找不到变量，按空串 \"\" 来替换。\n     * @return 格式化之后的具体文本内容。\n     */\n    public static String format(String str, final Map<String, Object> args, final boolean nullAsEmpty) {\n        ValueSource<String, String> values = key -> {\n            Object value = null;\n            if (args != null) {\n                value = args.get(key);\n            }\n            if (nullAsEmpty && value == null) {\n                value = \"\";\n            }\n            return Objects.toString(value);\n        };\n        return format(str, values, \"${\", \"}\", null);\n    }\n\n    public static String format(String str, final Map<String, Object> args) {\n        return format(str, args, false);\n    }\n\n    /**\n     * 与 format(String str, final Map args) 方法作用一样，\n     * 唯一的区别是变量值是从<code>ValueSource</code>对象中取。<br>\n     * \n     * @param str 模板内容。\n     * @param values 变量值。\n     * @return 格式化之后的具体文本内容。\n     */\n    public static String format(String str, ValueSource<String, String> values) {\n        return format(str, values, \"${\", \"}\", null);\n    }\n\n    /**\n     * 与 format(String str, final Map args) 方法作用一样， 但可以更灵活可定制化，\n     * 区别是: <br>\n     * 1. 变量值是从<code>ValueSource</code>对象中取。<br>\n     * 2. 可以用 begin 和 end 来自定义变量在模板内容中的表达格式，如：\n     * begin = \"#[\", end = \"]\" 表示变量是 #[name] 这类的格式，而不是传统的 ${name} 。<br>\n     * 3. 可以收集哪些变量没有被替换掉的，没有被替换掉的被放在一个叫\n     * notMatched 的List类型的参数中。<br>\n     * \n     * @param str 模板内容。\n     * @param values 变量值。\n     * @param begin 变量包裹的起始符。\n     * @param end 变量包裹的结束符。\n     * @param notMatched 被替换掉的变量的key.\n     * @return 格式化之后的具体文本内容。\n     */\n    public static String format(String str, ValueSource<String, String> values, String begin, String end,\n            List<String> notMatched) {\n        if (str == null || str.trim().length() == 0) {\n            return str;\n        }\n\n        if (begin == null || begin.trim().length() == 0 || end == null || end.trim().length() == 0) {\n            throw new NullPointerException(\"begin or end is null or empty：\" + begin + \", \" + end);\n        }\n\n        StringBuffer sb = new StringBuffer();\n        final int size = str.length();\n        final int beginLength = begin.length();\n        final int endLength = end.length();\n        int from = 0;\n        while (true) {\n            int n = str.indexOf(end, from);\n            //存在嵌套的时候，会错过第一个变量。例如 {a{b} {c}} b会被错过。\n            int m = (n == -1 ? str.indexOf(begin, from) : str.lastIndexOf(begin, n));\n            if(m <= from)m = -1;\n\n            if (m >= 0 && m < size && n > m && n < size) {\n                String s0 = str.substring(from, m);\n                sb.append(s0);\n                String sMatch = str.substring(m, n + endLength);\n                String key = sMatch.substring(beginLength, sMatch.length() - endLength);\n                String value = values.get(key);\n                if (value != null) {\n                    sb.append(value);\n                } else {\n                    sb.append(sMatch);\n                    if (notMatched != null) {\n                        notMatched.add(key);\n                    }\n                }\n                from = n + endLength;\n            } else {\n                break;\n            }\n        }\n        if (from < size) {\n            sb.append(str.substring(from));\n        }\n\n        return sb.toString();\n    }\n\n    /**\n     * 按空格分割字符串成数组，会对每个数组元素进行 trim 操作，并丢弃掉为“空串”的元素。\n     * \n     * @param content 字符串内容。\n     * @return trim后的字符串数组。\n     */\n    public static String[] splitWithTrim(String content) {\n        return splitWithTrim(content, -1);\n    }\n\n    /**\n     * 按指定的表达式 regex 进行分割字符串成数组，会对每个数组元素进行 trim 操作，并丢弃掉为“空串”的元素。\n     * \n     * @param content 字符串内容。\n     * @param regex 指定的表达式\n     * @return trim后的字符串数组。\n     */\n    public static String[] splitWithTrim(String content, String regex) {\n        return splitWithTrim(content, regex, -1);\n    }\n\n    /**\n     * 按空格分割字符串成数组，会对每个数组元素进行 trim 操作，并丢弃掉为“空串”的元素。<br>\n     * 与<code>splitWithTrim(String content)</code>方法不同的时，可以指定分割最大数量。\n     * \n     * @param content 字符串内容。\n     * @param limit 分割最大数量\n     * @return trim后的字符串数组。\n     */\n    public static String[] splitWithTrim(String content, int limit) {\n        if (content == null) {\n            return null;\n        }\n        return splitWithTrim(content, \" \", limit);\n    }\n\n    /**\n     * 按指定的表达式 regex 进行分割字符串成数组，会对每个数组元素进行 trim 操作，并丢弃掉为“空串”的元素。<br>\n     * 可以指定分割最大数量。<br>\n     * 比如：content = \"abc xxx xxxdef xxx ghi\", regex = \"xxx\", limit = 2; <br>\n     * 返回的结果为： [\"abc\", \"def xxx ghi\"]。\n     * \n     * @param content 字符串内容。\n     * @param regex 指定的表达式\n     * @param limit 分割最大数量\n     * @return trim后的字符串数组。\n     */\n    public static String[] splitWithTrim(String content, String regex, int limit) {\n        if (content == null) {\n            return null;\n        }\n\n        List<String> result = new ArrayList<>();\n        String[] strs = content.split(regex, limit);\n        if (strs != null) {\n            for (String str : strs) {\n                str = str.trim();\n                if (StringUtils.isEmpty(str)) {\n                    continue;\n                }\n                result.add(str);\n            }\n        }\n        if (limit >= 0 && result.size() < limit) {\n            int leftLimit = limit - result.size();\n            int lastIndex = result.size() - 1;\n            String lastContent = result.get(lastIndex);\n            String[] leftStrs = splitWithTrim(lastContent, regex, leftLimit);\n            if (leftStrs != null && leftStrs.length > 0) {\n                result.remove(lastIndex);\n                result.addAll(Arrays.asList(leftStrs));\n            }\n        }\n\n        return result.toArray(new String[result.size()]);\n    }\n\n    /**\n     * 将字节数组转成16进制描述的字符串。\n     * \n     * @param data 字节数组\n     * @return 16进制描述的字符串\n     */\n    public static String toHexString(byte[] data) {\n        if (data == null || data.length == 0) {\n            return null;\n        }\n\n        StringBuilder sb = new StringBuilder();\n        for (int i = 0; i < data.length; i++) {\n            // 取出字节的高四位 作为索引得到相应的十六进制标识符 注意无符号右移\n            sb.append(HEX_CHAR[(data[i] & 0xf0) >>> 4]);\n            // 取出字节的低四位 作为索引得到相应的十六进制标识符\n            sb.append(HEX_CHAR[(data[i] & 0x0f)]);\n        }\n        return sb.toString();\n    }\n\n    /**\n     * 将字节数组转成16进制描述的字符串。\n     * \n     * @param str 字节数组\n     * @return 16进制描述的字符串\n     */\n    public static byte[] fromHexString(String str) {\n        if (StringUtils.isEmpty(str)) {\n            return null;\n        }\n        str = str.trim().toLowerCase();\n\n        if (str.length() % 2 == 1) {\n            throw new InvalidParameterException(\"无效的16进制字符串：\" + str);\n        }\n        byte[] data = new byte[str.length() / 2];\n\n        for (int n = 0, i = 0; i < str.length() - 1; n++, i = i + 2) {\n            int lowValue = hexToNumber(i + 1, str);\n            int highValue = hexToNumber(i, str);\n            int value = (highValue * 16) + lowValue;\n            data[n] = (byte) value;\n        }\n        return data;\n    }\n\n    private static final int hexToNumber(int i, String str) {\n        char c = str.charAt(i);\n        int value = (c >= 'a') ? (c - 'a' + 10) : (c - '0');\n        if (value < 0 || value > HEX_CHAR.length - 1) {\n            String msg = String.format(\"无效的16进制字符串：%s, 第%d个字符不合法: %s\", str, i + 1, c);\n            throw new InvalidParameterException(msg);\n        }\n        return value;\n    }\n\n    public static final String toString(byte[] bytes) {\n        if (bytes == null) {\n            return null;\n        }\n        StringBuffer sb = new StringBuffer();\n        sb.append(\"[\");\n        for (int i = 0; i < bytes.length; i++) {\n            if (i > 0) {\n                sb.append(\",\");\n            }\n            sb.append(bytes[i]);\n        }\n        sb.append(\"]\");\n        return sb.toString();\n    }\n\n    /**\n     * 检测指定的 path 是否匹配 regexPaths 。\n     * \n     * @param path 路径。\n     * @param regexPaths 需要匹配的路径。\n     * @return 匹配的路径。\n     */\n    public static boolean match(String path, String... regexPaths) {\n        PathMatcher pathMatch = new AntPathMatcher();\n        if (path == null || regexPaths == null) {\n            return false;\n        }\n        for (String regexPath : regexPaths) {\n            if (regexPath == null) {\n                continue;\n            }\n            regexPath = regexPath.trim();\n            path = path.trim();\n            if (pathMatch.match(regexPath, path)) {\n                return true;\n            }\n        }\n        return false;\n    }\n    \n    /**\n     * 将字符串分割成 Map 形式。\n     * @param content 字符串内容\n     * @param split 分割符\n     * @param joiner key与value之间的连接符。\n     * @return map对象。\n     */\n    public static Map<String, String> toMap(String content, String split, String joiner) {\n        String[] array = splitWithTrim(content, split);\n        if (array == null || array.length == 0) {\n            return null;\n        }\n        \n        Map<String, String> result = new HashMap<String, String>();\n        for (String item : array) {\n            if (item == null || item.trim().length() == 0) {\n                continue;\n            }\n            int i = item.indexOf(joiner);\n            if (i <= 0 || i > item.length() - 1) {\n                if (log.isWarnEnabled()) {\n                    log.warn(\"unresolve[\" + item + \"] \" + \"for experssion: \" + item);\n                }\n            }\n            String key = item.substring(0, i).trim();\n            String value = item.substring(i + joiner.length()).trim();\n            result.put(key, value);\n        }\n        return result;\n    }\n\n\n    /**\n     * 将数字变为固定长度的字符串，不足位补充0\n     * @param number\n     * @param length\n     * @return\n     */\n    public static String formatString(int number, int length){\n        String n = number+\"\";\n        StringBuffer sb = new StringBuffer();\n        sb.setLength(length);\n        int start = 0;\n        for(int i = length -1 ; i >= 0 ; i -- ){\n            int nlen = n.length() - 1;\n            char c = '0';\n            if(nlen - start >= 0 )\n                c = n.charAt(nlen - start);\n            sb.setCharAt(i, c);\n            start ++;\n        }\n        return  sb.toString();\n    }\n\n    /**\n     * 判断字符串是否为空\n     * @param string\n     * @return\n     */\n    public static boolean isNull(String string){\n        if(string ==null)return true;\n        if(string.trim().equals(\"\"))return true;\n        return false;\n    }\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/config/ConfigElement.java",
    "content": "package com.terran4j.commons.util.config;\n\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\nimport com.terran4j.commons.util.value.ValueSource;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\n\npublic interface ConfigElement extends ValueSource<String, String> {\n\n    int size();\n\n    default String get(String key) {\n        String[] array = key.split(\"\\\\.\");\n        ConfigElement current = this;\n        for (String item : array) {\n            ConfigElement element = null;\n            try {\n                element = current.getChild(item);\n            } catch (BusinessException e) {\n                return null;\n            }\n            if (element == null) {\n                return null;\n            }\n            current = element;\n        }\n        return current.getValue();\n    }\n\n    String attr(String attrName);\n\n    default String attr(String attrKey, String defaultValue) {\n        String value = attr(attrKey);\n        if (value == null) {\n            value = defaultValue;\n        }\n        return value;\n    }\n\n    default int attr(String attrKey, int defaultValue) {\n        String attrValue = attr(attrKey);\n        if (attrValue == null || attrValue.trim().length() == 0) {\n            return defaultValue;\n        }\n        return Integer.parseInt(attrValue);\n    }\n\n    default Integer attrAsInt(String attrKey) {\n        String attrValue = attr(attrKey);\n        if (attrValue == null || attrValue.trim().length() == 0) {\n            return null;\n        }\n        return Integer.parseInt(attrValue);\n    }\n\n    default Long attrAsLong(String attrKey) {\n        String attrValue = attr(attrKey);\n        if (attrValue == null || attrValue.trim().length() == 0) {\n            return null;\n        }\n        return Long.parseLong(attrValue);\n    }\n\n    default Double attrAsDouble(String attrKey) {\n        String attrValue = attr(attrKey);\n        if (attrValue == null || attrValue.trim().length() == 0) {\n            return null;\n        }\n        return Double.parseDouble(attrValue);\n    }\n\n    ConfigElement[] getChildren();\n\n    ConfigElement[] getChildren(String elementName);\n\n    String getValue();\n\n    default boolean attr(String attrKey, boolean defaultValue) {\n        String attrValue = attr(attrKey);\n        if (attrValue == null || attrValue.trim().length() == 0) {\n            return defaultValue;\n        }\n        return Boolean.parseBoolean(attrValue);\n    }\n\n    default Boolean attrAsBoolean(String attrKey) {\n        String attrValue = attr(attrKey);\n        if (attrValue == null || attrValue.trim().length() == 0) {\n            return null;\n        }\n        return Boolean.parseBoolean(attrValue);\n    }\n\n    ClassLoader getClassLoader();\n\n    default <T> T attr(String attrName, Class<T> clazz) throws BusinessException {\n        ConfigElement attrValue = getChild(attrName);\n        if (attrValue == null) {\n            return null;\n        }\n        return attrValue.asObject(clazz);\n    }\n\n    default <T> List<T> getChildren(String attrKey, Class<T> clazz) throws BusinessException {\n        ConfigElement[] children = getChildren(attrKey);\n        if (children == null) {\n            return null;\n        }\n\n        List<T> result = new ArrayList<>();\n        if (children.length == 0) {\n            return result;\n        }\n\n        for (ConfigElement child : children) {\n            T item = child.asObject(clazz);\n            result.add(item);\n        }\n\n        return result;\n    }\n\n    <T> T asObject(Class<T> clazz) throws BusinessException;\n\n    ConfigElement getChild(String eleName) throws BusinessException;\n\n    default String getChildText(String eleName) throws BusinessException {\n        ConfigElement c = getChild(eleName);\n        if (c == null) {\n            return null;\n        }\n        return c.getValue();\n    }\n\n    String getName();\n\n    Set<String> attrSet();\n\n    default Object createObject(String classAttrKey) throws BusinessException {\n        String className = attr(classAttrKey);\n        try {\n            Class<?> clazz = getClassLoader().loadClass(className);\n            return clazz.newInstance();\n        } catch (ClassNotFoundException e) {\n            throw new BusinessException(CommonErrorCode.CONFIG_ERROR, e)\n                    .put(\"className\", className).put(\"element\", asText());\n        } catch (InstantiationException e) {\n            throw new BusinessException(CommonErrorCode.CONFIG_ERROR, e)\n                    .put(\"className\", className).put(\"element\", asText());\n        } catch (IllegalAccessException e) {\n            throw new BusinessException(CommonErrorCode.CONFIG_ERROR, e)\n                    .put(\"className\", className).put(\"element\", asText());\n        }\n    }\n\n    String asText();\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/config/JsonConfigElement.java",
    "content": "package com.terran4j.commons.util.config;\n\nimport com.google.gson.*;\nimport com.terran4j.commons.util.error.BusinessException;\n\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class JsonConfigElement implements ConfigElement {\n\n    private static final Gson gson = new Gson();\n\n    private static final JsonParser parser = new JsonParser();\n\n    private final ClassLoader classLoader;\n\n    private JsonElement element;\n\n    public JsonConfigElement(String jsonText) throws BusinessException {\n        classLoader = Thread.currentThread().getContextClassLoader();\n        try {\n            element = parser.parse(jsonText);\n        } catch (JsonSyntaxException e) {\n            // 不是 json 串，就按普通的字符串来处理。\n            element = new JsonPrimitive(jsonText);\n        }\n    }\n\n    public JsonConfigElement(JsonElement element, ClassLoader classLoader) {\n        super();\n        this.element = element;\n        this.classLoader = classLoader;\n    }\n\n    @Override\n    public int size() {\n        if (element == null) {\n            return 0;\n        }\n        if (!element.isJsonArray()) {\n            return 0;\n        }\n        return element.getAsJsonArray().size();\n    }\n\n    @Override\n    public String attr(String attrName) {\n        JsonObject jsonObject = asJsonObject(element);\n        if (jsonObject == null) {\n            return null;\n        }\n        JsonPrimitive jsonPrimitive = jsonObject.getAsJsonPrimitive(attrName);\n        if (jsonPrimitive == null) {\n            return null;\n        }\n        return jsonPrimitive.getAsString();\n    }\n\n    private JsonPrimitive asJsonPrimitive(JsonElement element) {\n        if (element == null) {\n            return null;\n        }\n        if (!element.isJsonPrimitive()) {\n            return null;\n        }\n        JsonPrimitive jsonPrimitive = element.getAsJsonPrimitive();\n        return jsonPrimitive;\n    }\n\n    @Override\n    public ConfigElement[] getChildren() {\n        JsonArray jsonArray = asJsonArray(element);\n        return toConfigElements(jsonArray);\n    }\n\n    private ConfigElement[] toConfigElements(JsonArray jsonArray) {\n        if (jsonArray == null) {\n            return null;\n        }\n        if (jsonArray.size() == 0) {\n            return new JsonConfigElement[0];\n        }\n        int size = jsonArray.size();\n        ConfigElement[] children = new JsonConfigElement[size];\n        for (int i = 0; i < size; i++) {\n            JsonElement child = jsonArray.get(i);\n            children[i] = new JsonConfigElement(child, classLoader);\n        }\n        return children;\n    }\n\n    private JsonArray asJsonArray(JsonElement element) {\n        if (element == null) {\n            return null;\n        }\n        if (!element.isJsonArray()) {\n            return null;\n        }\n        JsonArray jsonArray = element.getAsJsonArray();\n        return jsonArray;\n    }\n\n    private JsonObject asJsonObject(JsonElement element) {\n        if (element == null) {\n            return null;\n        }\n        if (!element.isJsonObject()) {\n            return null;\n        }\n        JsonObject jsonObject = element.getAsJsonObject();\n        if (jsonObject == null) {\n            return null;\n        }\n        return jsonObject;\n    }\n\n    @Override\n    public ConfigElement[] getChildren(String elementName) {\n        JsonObject jsonObject = asJsonObject(element);\n        if (jsonObject == null) {\n            return null;\n        }\n        JsonElement child = jsonObject.get(elementName);\n        JsonArray jsonArray = asJsonArray(child);\n        return toConfigElements(jsonArray);\n    }\n\n    @Override\n    public String getValue() {\n        JsonPrimitive jsonPrimitive = asJsonPrimitive(element);\n        if (jsonPrimitive == null) {\n            return null;\n        }\n        return jsonPrimitive.getAsString();\n    }\n\n    @Override\n    public ClassLoader getClassLoader() {\n        return classLoader;\n    }\n\n    @Override\n    public <T> T asObject(Class<T> clazz) throws BusinessException {\n        if (element == null) {\n            return null;\n        }\n        return gson.fromJson(element, clazz);\n    }\n\n    @Override\n    public ConfigElement getChild(String eleName) throws BusinessException {\n        JsonObject jsonObject = asJsonObject(element);\n        if (jsonObject == null) {\n            return null;\n        }\n        JsonElement child = jsonObject.get(eleName);\n        if (child == null) {\n            return null;\n        }\n        return new JsonConfigElement(child, classLoader);\n    }\n\n    @Override\n    public String getName() {\n        // 对于 JSON 中的元素，本来就没有名字。\n        return null;\n    }\n\n    @Override\n    public Set<String> attrSet() {\n        JsonObject jsonObject = asJsonObject(element);\n        if (jsonObject == null) {\n            return null;\n        }\n        Set<String> keys = new HashSet<>();\n        Set<Map.Entry<String, JsonElement>> entries = jsonObject.entrySet();\n        Iterator<Map.Entry<String, JsonElement>> it = entries.iterator();\n        while (it.hasNext()) {\n            Map.Entry<String, JsonElement> entry = it.next();\n            if (entry == null) {\n                continue;\n            }\n            String key = entry.getKey();\n            keys.add(key);\n        }\n        return keys;\n    }\n\n    @Override\n    public String asText() {\n        if (element == null) {\n            return null;\n        }\n        return gson.toJson(element);\n    }\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/config/XmlConfigElement.java",
    "content": "package com.terran4j.commons.util.config;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\n\nimport javax.xml.parsers.DocumentBuilderFactory;\n\nimport org.springframework.util.StringUtils;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.NamedNodeMap;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.w3c.dom.Text;\n\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\nimport com.terran4j.commons.util.error.ErrorCodes;\n\npublic class XmlConfigElement implements ConfigElement {\n\n\tprivate static final String T = \"    \";\n\n\tprivate final ClassLoader classLoader;\n\n\tprivate Element element;\n\t\n\tpublic XmlConfigElement(String xmlContent) throws BusinessException {\n\t    classLoader = Thread.currentThread().getContextClassLoader();\n\t    InputStream in = Strings.toInputStream(xmlContent);\n\t    try {\n            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in);\n            element = doc.getDocumentElement();\n        } catch (Exception e) {\n            throw new BusinessException(CommonErrorCode.XML_ERROR, e).put(\"xmlContent\", xmlContent)\n                    .setMessage(\"parse xml file error, xmlContent:\\n{xmlContent}\");\n        } finally {\n            if (in != null) {\n                try {\n                    in.close();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n\t}\n\n\tpublic XmlConfigElement(Class<?> clazz, String fileName) throws BusinessException {\n\t\tsuper();\n\t\tclassLoader = clazz.getClassLoader();\n\t\tInputStream in = classLoader.getResourceAsStream(fileName);\n\t\tif (in == null) {\n\t\t\tthrow new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND)\n\t\t\t\t\t.put(\"fileName\", fileName).put(\"classpath\", clazz.getPackage().getName())\n\t\t\t\t\t.setMessage(\"xml file not found in classpath, fileName = ${fileName}, in classpath: ${classpath}\");\n\t\t}\n\t\t\n\t\ttry {\n\t\t\tDocument doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in);\n\t\t\telement = doc.getDocumentElement();\n\t\t} catch (Exception e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.XML_ERROR, e)\n\t\t\t\t\t.put(\"fileName\", fileName).put(\"classpath\", clazz.getPackage().getName())\n\t\t\t\t\t.setMessage(\"parse xml file error, fileName = ${fileName}, in classpath: ${classpath}\");\n\t\t} finally {\n\t\t\tif (in != null) {\n\t\t\t\ttry {\n\t\t\t\t\tin.close();\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\tpublic XmlConfigElement(Element element) {\n\t\tsuper();\n\t\tthis.element = element;\n\t\tclassLoader = getClass().getClassLoader();\n\t}\n\n    @Override\n    public int size() {\n        return 0; // XML 没有数组结构，因此个数一律返回 0 .\n    }\n\n    public String attr(String attriName) {\n\t\tString value = element.getAttribute(attriName);\n\t\tif (StringUtils.isEmpty(value)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn value;\n\t}\n\n\tpublic ConfigElement[] getChildren() {\n\t\tNodeList nodeList = element.getChildNodes();\n\t\tif (nodeList == null) {\n\t\t\treturn new ConfigElement[0];\n\t\t}\n\n\t\tList<ConfigElement> list = new ArrayList<ConfigElement>();\n\t\tfor (int i = 0; i < nodeList.getLength(); i++) {\n\t\t\tNode node = nodeList.item(i);\n\t\t\tif (node instanceof Element) {\n\t\t\t\tElement childElement = (Element) node;\n\t\t\t\tConfigElement c = new XmlConfigElement(childElement);\n\t\t\t\tlist.add(c);\n\t\t\t}\n\t\t}\n\n\t\treturn list.toArray(new ConfigElement[list.size()]);\n\t}\n\n\tpublic ConfigElement[] getChildren(String eleName) {\n\t\tNodeList nodeList = element.getChildNodes();\n\t\tif (nodeList == null) {\n\t\t\treturn new ConfigElement[0];\n\t\t}\n\n\t\tList<ConfigElement> list = new ArrayList<ConfigElement>();\n\t\tfor (int i = 0; i < nodeList.getLength(); i++) {\n\t\t\tNode node = nodeList.item(i);\n\t\t\tif (node instanceof Element) {\n\t\t\t\tElement childElement = (Element) node;\n\t\t\t\tif (eleName == null || eleName.equals(childElement.getTagName())) {\n\t\t\t\t\tConfigElement c = new XmlConfigElement(childElement);\n\t\t\t\t\tlist.add(c);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn list.toArray(new ConfigElement[list.size()]);\n\t}\n\n\tpublic String getValue() {\n\t\tNodeList nodeList = element.getChildNodes();\n\t\tif (nodeList == null || nodeList.getLength() == 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfor (int i = 0; i < nodeList.getLength(); i++) {\n\t\t\tNode node = nodeList.item(i);\n\t\t\tif (node instanceof Text) {\n\t\t\t\tText text = (Text) node;\n\t\t\t\tString str = text.getNodeValue();\n\t\t\t\tif (StringUtils.isEmpty(str)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tsb.append(str.trim()).append(\"\\n\");\n\t\t\t}\n\t\t}\n\n\t\tString value = sb.toString().trim();\n\t\tif (StringUtils.isEmpty(value)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic ClassLoader getClassLoader() {\n\t\treturn this.classLoader;\n\t}\n\n    @Override\n    public <T> T asObject(Class<T> clazz) throws BusinessException {\n        throw new UnsupportedOperationException(\"XmlConfigElement can't as Object!\");\n    }\n\n    public ConfigElement getChild(String eleName) throws BusinessException {\n\t\tConfigElement[] childElements = getChildren(eleName);\n\t\tif (childElements != null && childElements.length > 0) {\n\t\t\tif (childElements.length > 1) {\n\t\t\t\tStringBuffer sb = new StringBuffer();\n\t\t\t\tfor (ConfigElement e : childElements) {\n\t\t\t\t\tsb.append(\"\\n\").append(e.getValue());\n\t\t\t\t}\n\t\t\t\tthrow new BusinessException(ErrorCodes.CONFIG_ERROR)\n\t\t\t\t\t\t.put(\"childrenElementName\", eleName)\n\t\t\t\t\t\t.put(\"element\", asText())\n\t\t\t\t\t\t.setMessage(\"Children Elment must NOT more than one\");\n\t\t\t}\n\t\t\treturn childElements[0];\n\t\t}\n\t\treturn null;\n\t}\n\n\n\n\tpublic String getName() {\n\t\treturn element.getTagName();\n\t}\n\n\tpublic String asText() {\n\t\treturn toString(0);\n\t}\n\n\tpublic final Element getElement() {\n\t\treturn element;\n\t}\n\n\tpublic final void setElement(Element element) {\n\t\tthis.element = element;\n\t}\n\n\tpublic String toString() {\n\t\treturn toString(0);\n\t}\n\n\tpublic Set<String> attrSet() {\n\t\tif (element == null) {\n\t\t\treturn null;\n\t\t}\n\t\tSet<String> keys = new HashSet<String>();\n\t\tNamedNodeMap map = element.getAttributes();\n\t\tfor (int i = 0; i < map.getLength(); i++) {\n\t\t\tNode node = map.item(i);\n\t\t\tString key = node.getNodeName();\n\t\t\tkeys.add(key);\n\t\t}\n\t\treturn keys;\n\t}\n\n\tprivate final String toString(int indent) {\n\t\tStringBuffer isb = new StringBuffer(\"\");\n\t\tfor (int i = 0; i < indent; i++) {\n\t\t\tisb.append(T);\n\t\t}\n\t\tString is = isb.toString();\n\n\t\tStringBuffer sb = new StringBuffer();\n\t\tsb.append(is).append(\"<\").append(getName());\n\n\t\t// 属性输出\n\t\tIterator<String> it = attrSet().iterator();\n\t\twhile (it.hasNext()) {\n\t\t\tString key = it.next();\n\t\t\tString value = attr(key);\n\t\t\tsb.append(\" \").append(key).append(\"=\\\"\").append(value).append(\"\\\"\");\n\t\t}\n\n\t\tString value = getValue();\n\t\tConfigElement[] children = getChildren();\n\n\t\tif ((value == null || value.length() == 0) && (children == null || children.length == 0)) {\n\t\t\tsb.append(\"/>\");\n\t\t\treturn sb.toString();\n\t\t}\n\t\tsb.append(\">\\n\");\n\n\t\tif (value != null && value.trim().length() > 0) {\n\t\t\tsb.append(is).append(T).append(value.trim()).append(\"\\n\");\n\t\t}\n\n\t\tif (children != null && children.length > 0) {\n\t\t\tfor (ConfigElement child : children) {\n\t\t\t\tif (child instanceof XmlConfigElement) {\n\t\t\t\t\tXmlConfigElement xmlElement = (XmlConfigElement) child;\n\t\t\t\t\tString str = xmlElement.toString(indent + 1);\n\t\t\t\t\tsb.append(str).append(\"\\n\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tsb.append(is).append(\"</\").append(getName()).append(\">\");\n\n\t\treturn sb.toString();\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/AuthorizedException.java",
    "content": "package com.terran4j.commons.util.error;\n\nimport java.util.Locale;\n\npublic class AuthorizedException extends BusinessException{\n    public AuthorizedException(String code) {\n        super(code);\n    }\n\n    public AuthorizedException(String code, Object... args) {\n        super(code, args);\n    }\n\n    public AuthorizedException(String code, Throwable e) {\n        super(code, e);\n    }\n\n    public AuthorizedException(String code, Locale locale, Throwable e, Object... args) {\n        super(code, locale, e, args);\n    }\n\n    public AuthorizedException(ErrorCode code, Throwable cause) {\n        super(code, cause);\n    }\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/BusinessException.java",
    "content": "package com.terran4j.commons.util.error;\n\nimport java.io.IOException;\nimport java.util.Enumeration;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.MissingResourceException;\nimport java.util.ResourceBundle;\nimport java.util.Stack;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.util.value.ResourceBundlesProperties;\nimport com.terran4j.commons.util.value.RichProperties;\nimport org.springframework.web.bind.annotation.ResponseStatus;\n\n/**\n * 业务异常基类。\n * \n * @author jiangwei\n */\npublic class BusinessException extends Exception {\n\n\tprivate static final long serialVersionUID = 5988465338967853686L;\n\n\t/**\n\t * 异常信息对应的文案资源。\n\t */\n\tprivate static final Map<Class<? extends ErrorCode>, ResourceBundle> bundles = new ConcurrentHashMap<>();\n\n\t/**\n\t * 表示不存在的ResourceBundle\n\t */\n\tprivate static final ResourceBundle NONEXISTENT_BUNDLE = new ResourceBundle() {\n        public Enumeration<String> getKeys() { return null; }\n        protected Object handleGetObject(String key) { return null; }\n        public String toString() { return \"NONEXISTENT_BUNDLE\"; }\n    };\n\n\tprivate final Stack<RichProperties> info = new Stack<RichProperties>();\n\t\n\tprivate final ErrorCode code;\n\t\n\tpublic BusinessException(String code) {\n\t\tthis(code, Locale.getDefault(), new RuntimeException(code));\n\t}\n\n\tpublic BusinessException(String code, Object... args) {\n\t\tthis(code, Locale.getDefault(), new RuntimeException(code), args);\n\t}\n\t\n\tpublic BusinessException(String code, Throwable e) {\n\t\tthis(code, Locale.getDefault(), e);\n\t}\n\n\tprivate Object[] args;\n\n\tpublic BusinessException(String code, Locale locale, Throwable e, Object... args) {\n\t\tsuper(e);\n\t\tthis.code = new ResourceErrorCode(code, locale);\n\t\tinfo.push(new RichProperties());\n\t\tString message = getMessage(code, locale, args);\n\t\tif (StringUtils.hasText(message)) {\n\t\t\tinfo.peek().setMessage(message);\n\t\t}\n\t\tthis.args = args;\n\t}\n\n\tpublic void setLocale(Locale locale){\n\t\tString message = getMessage(this.code.getName(), locale, args);\n\t\tif (StringUtils.hasText(message)) {\n\t\t\tinfo.peek().setMessage(message);\n\t\t}\n\t}\n\t\n\tpublic static final String getMessage(String code) {\n\t\treturn getMessage(code, Locale.getDefault());\n\t}\n\t\n\tpublic static final String getMessage(String code, Locale locale, Object... args) {\n\t\tResourceBundlesProperties props = null;\n\t\ttry {\n\t\t\tprops = ResourceBundlesProperties.get(\"error\", locale);\n\n\t\t} catch (IOException e1) {\n\t\t\tthrow new RuntimeException(e1);\n\t\t}\n\t\tif (props != null && props.get(code) != null) {\n\t\t\treturn props.get(code, args);\n\t\t}\n\t\treturn null;\n\t}\n\n\t@Deprecated\n\tpublic BusinessException(ErrorCode code) {\n\t\tsuper();\n\t\tthis.code = code;\n\t\tinfo.push(new RichProperties());\n\t\tinfo.peek().setMessage(getResource(code.getName()));\n\t}\n\t\n\tpublic BusinessException(ErrorCode code, Throwable cause) {\n\t\tsuper(cause);\n\t\tthis.code = code;\n\t\tinfo.push(new RichProperties());\n\t\tinfo.peek().setMessage(getResource(code.getName()));\n\t}\n\t\n\tpublic final BusinessException reThrow(String message) {\n\t\tRichProperties newInfo = new RichProperties().setMessage(message);\n\t\tinfo.push(newInfo);\n\t\treturn this;\n\t}\n\t\n\t/**\n\t * 获取异常的描述信息。\n\t */\n\t@Override\n\tpublic String getMessage() {\n\t\treturn getInfo().getMessage();\n\t}\n\t\n\tpublic final BusinessException setMessage(String message) {\n\t\tgetInfo().setMessage(message);\n\t\treturn this;\n\t}\n\n\tpublic final BusinessException put(String key, Object value) {\n\t\tgetInfo().put(key, value);\n\t\treturn this;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\tpublic <T extends BusinessException> T as(Class<T> clazz) {\n\t\tif (clazz != getClass()) {\n\t\t\tString msg = String.format(\"clazz must be: {}\", getClass().getName());\n\t\t\tthrow new IllegalArgumentException(msg);\n\t\t}\n\t\treturn (T) this;\n\t}\n\n\tpublic final Object get(String key) {\n\t\treturn getInfo().get(key);\n\t}\n\n\tpublic ErrorCode getErrorCode() {\n\t\treturn code;\n\t}\n\t\n\tRichProperties getInfo() {\n\t\treturn info.peek();\n\t}\n\t\n\tStack<RichProperties> getInfoStack() {\n\t\treturn info;\n\t}\n\t\n\t/**\n\t * \n\t * @return\n\t */\n\tprivate final ResourceBundle getBundle() {\n\t\tResourceBundle bundle = bundles.get(code.getClass());\n\t\tif (bundle != null) {\n\t\t\treturn bundle == NONEXISTENT_BUNDLE ? null : bundle;\n\t\t}\n\t\t\n\t\tString path = code.getClass().getName().replace('.', '/');\n\t\ttry {\n\t\t    bundle = ResourceBundle.getBundle(path);\n\t\t} catch (MissingResourceException e) {\n\t\t    // ignore.\n        }\n\t\tif (bundle == null) {\n\t\t    path = code.getClass().getSimpleName();\n\t\t    try {\n\t            bundle = ResourceBundle.getBundle(path);\n\t        } catch (MissingResourceException e) {\n\t            // ignore.\n\t        }\n\t\t}\n\t\t\n\t\t// 缓存 bundle 对象。\n\t\tbundles.put(code.getClass(), bundle == null ? NONEXISTENT_BUNDLE : bundle);\n\n\t\treturn bundle;\n\t}\n\n\tprivate String getResource(String key) {\n\t\tif (StringUtils.isEmpty(key)) {\n\t\t\treturn null;\n\t\t}\n\t\tkey = key.trim();\n\n\t\tString message = null;\n\t\tResourceBundle bundle = getBundle();\n\t\tif (bundle != null) {\n\t\t    try {\n\t\t        message = bundle.getString(key);\n\t\t    } catch (MissingResourceException e) {\n\t\t        // ignore.\n            }\n\t\t}\n\n\t\tif (StringUtils.isEmpty(message)) {\n\t\t\tmessage = key.replace('.', ' ');\n\t\t}\n\t\treturn message;\n\t}\n\n\t/**\n\t * 获取异常的详细信息。\n\t * \n\t * @return 异常详细信息。\n\t */\n\tpublic ErrorReport getReport() {\n\t\tErrorReport topReport = null;\n\t\tThrowable currentThrowable = this;\n\t\tErrorReport currentReport = null;\n\t\tErrorReport previousReport = null;\n\t\twhile (currentThrowable != null) {\n\t\t\tcurrentReport = new ErrorReport(currentThrowable);\n\t\t\tif (topReport == null) {\n\t\t\t\ttopReport = currentReport;\n\t\t\t}\n\t\t\tif (previousReport != null) {\n\t\t\t\tpreviousReport.setCause(currentReport);\n\t\t\t}\n\t\t\tpreviousReport = currentReport;\n\t\t\tcurrentThrowable = currentThrowable.getCause();\n\t\t}\n\t\treturn topReport;\n\t}\n\t\n\tpublic Map<String, Object> getProps() {\n\t\treturn getInfo().getAll();\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/CommonErrorCode.java",
    "content": "package com.terran4j.commons.util.error;\n\npublic enum CommonErrorCode implements ErrorCode {\n\n\t/**\n\t * 请求参数非法\n\t */\n\tUNKNOWN_ERROR(1, ErrorCodes.UNKNOWN_ERROR),\n\t\n\t/**\n\t * 请求参数非法\n\t */\n\tINVALID_PARAM(2, ErrorCodes.INVALID_PARAM, \"key, value\"),\n\t\n\t/**\n\t * 请求参数为空。\n\t */\n\tNULL_PARAM(3, ErrorCodes.NULL_PARAM, \"key\"),\n\t\n\t/**\n\t * 配置错误。\n\t */\n\tCONFIG_ERROR(4, ErrorCodes.CONFIG_ERROR),\n\t\n\t/**\n\t * 解析XML出错。\n\t */\n\tXML_ERROR(5, \"xml.error\"),\n\t\n\t/**\n\t * 资源找不到。\n\t */\n\tRESOURCE_NOT_FOUND(6, ErrorCodes.RESOURCE_NOT_FOUND, \"type, keyName, keyValue\"),\n\t\n\t/**\n\t * 解析 json 串出错。\n\t */\n\tJSON_ERROR(7, \"json.error\", \"jsonText\"),\n\t\n\t/**\n\t * 参数太长了\n\t */\n\tPARAM_LENGTH_TOO_LONG(8, \"param.length.too.long\", \"key, maxLength, acutalLength\"),\n\t\n\t/**\n\t * 关键字段重复。\n\t */\n\tDUPLICATE_KEY(9, ErrorCodes.DUPLICATE_KEY, \"key, value\"),\n\t\n\t/**\n\t * 内部错误。\n\t */\n\tINTERNAL_ERROR(10, ErrorCodes.INTERNAL_ERROR),\n\t;\n\t\n\tpublic static final String KEY = \"key\";\n\t\n\tpublic static final String VALUE = \"value\";\n\t\n\tpublic static final String MAX_LENGTH = \"maxLength\";\n\t\n\tpublic static final String ACUTAL_LENGTH = \"acutalLength\";\n\t\n\tprivate final int value;\n\t\n\tprivate final String name;\n\t\n\tprivate final String[] requiredFields;\n\n\tprivate CommonErrorCode(int value, String name) {\n\t\tthis.value = value;\n\t\tthis.name = name;\n\t\tthis.requiredFields = new String[]{\"\"};\n\t}\n\t\n\tprivate CommonErrorCode(int value, String name, String[] requiredFields) {\n\t\tthis.value = value;\n\t\tthis.name = name;\n\t\tthis.requiredFields = requiredFields;\n\t}\n\t\n\tprivate CommonErrorCode(int value, String name, String requiredFields) {\n\t\tthis.value = value;\n\t\tthis.name = name;\n\t\tthis.requiredFields = ErrorCode.toArray(requiredFields);\n\t}\n\n\tpublic final int getValue() {\n\t\treturn value;\n\t}\n\t\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\n\tpublic final String[] getRequiredFields() {\n\t\treturn requiredFields;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/ErrorCode.java",
    "content": "package com.terran4j.commons.util.error;\n\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.util.Strings;\n\n/**\n * \n * \n * @author jiangwei\n */\npublic interface ErrorCode {\n\n\tint getValue();\n\t\n\tString getName();\n\t\n\tdefault String getMessage() {\n\t\treturn null;\n\t}\n\t\n\tdefault String[] getRequiredFields() {\n\t\treturn null;\n\t}\n\t\n\tstatic String[] toArray(String keys) {\n\t\tif (StringUtils.isEmpty(keys)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn Strings.splitWithTrim(keys);\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/ErrorCodes.java",
    "content": "package com.terran4j.commons.util.error;\n\n/**\n * \n * @author jiangwei\n *\n */\npublic interface ErrorCodes {\n\n\t// 400 \n\t/**\n\t * 用户发出的请求有错误，服务器没有进行新建或修改数据的操作，该操作是幂等的。\n\t */\n\tpublic static final String INVALID_REQUEST = \"invalid.request\";\n\n\t/**\n\t * 无效的请求参数。\n\t */\n\tpublic static final String INVALID_PARAM = \"invalid.param\";\n\n\t/**\n\t * 必填的参数为空。\n\t */\n\tpublic static final String NULL_PARAM = \"null.param\";\n\n\t/**\n\t * 内容非法, 有违禁词等\n\t */\n\tpublic static final String BLACKLISTED_INPUT = \"blacklisted.input\";\n\n\t// 401\n\t/**\n\t * 表示用户没有权限（令牌、用户名、密码错误）。\n\t */\n\tpublic static final String AUTH_FAILED = \"auth.failed\";\n\n\t// 403\n\t/**\n\t * 表示用户得到授权（与401错误相对），但是访问是被禁止的。\n\t */\n\tpublic static final String ACCESS_FORBIDDEN = \"access.forbidden\";\n\n\tpublic static final String ACCESS_DENY = \"access.deny\";\n\n\t// 404\n\t/**\n\t * 用户发出的请求针对的是不存在的记录，服务器没有进行操作，该操作是幂等的。\n\t */\n\tpublic static final String RESOURCE_NOT_FOUND = \"resource.not.found\";\n\n\t// 422\n\t/**\n\t * 当创建一个对象时，发生一个验证错误。\n\t */\n\tpublic static final String UNPROCESABLE_ENTITY = \"unprocesable.entity\";\n\n\t/**\n\t * 当创建或更新对象时，对象的关键字段值重复。\n\t */\n\tpublic static final String DUPLICATE_KEY = \"duplicate.key\";\n\n\t// 500\n\t/**\n\t * 服务器内部错误，用户将无法判断发出的请求是否成功。\n\t */\n\tpublic static final String INTERNAL_ERROR = \"internal.error\";\n\n\t/**\n\t * 服务器未知错误。\n\t */\n\tpublic static final String UNKNOWN_ERROR = \"unknown.error\";\n\t\n\t/**\n\t * 服务器配置错误。\n\t */\n\tpublic static final String CONFIG_ERROR = \"config.error\";\n\n}"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/ErrorMessage.java",
    "content": "package com.terran4j.commons.util.error;\n\nimport com.terran4j.commons.util.value.ResourceBundlesProperties;\nimport lombok.Data;\n\nimport java.io.IOException;\nimport java.util.Locale;\n\n@Data\npublic class ErrorMessage {\n    private String code;\n    private String message;\n\n    protected ErrorMessage(String code, String message) {\n        this.code = code;\n        this.message = message;\n    }\n\n    public static ErrorMessage from(Locale locale, String code, Object... args){\n        String message = \"\";\n        ResourceBundlesProperties props = null;\n        try {\n            props = ResourceBundlesProperties.get(\"error\", locale);\n        } catch (IOException e1) {\n            throw new RuntimeException(e1);\n        }\n        if (props != null && props.get(code) != null) {\n            message = props.get(code, args);\n        }\n        return new ErrorMessage(code, message);\n    }\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/ErrorReport.java",
    "content": "package com.terran4j.commons.util.error;\n\nimport java.util.Iterator;\nimport java.util.Stack;\n\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.value.RichProperties;\n\npublic class ErrorReport {\n\t\n\tprivate static final String COLON = \": \";\n\n\tprivate static final String INDENT = \"    \";\n\t\n\tprivate static final String N = \"\\n\";\n\n\tprivate final Throwable throwable;\n\n\tprivate final String className;\n\t\n\tprivate final String methodName;\n\t\n\tprivate final String fileName;\n\t\n\tprivate final int lineNumber;\n\t\n\tprivate final ErrorCode code;\n\t\n\tprivate final Stack<RichProperties> info;\n\t\n\tprivate ErrorReport cause;\n\t\n\tpublic ErrorReport(Throwable throwable) {\n\t\tsuper();\n\t\tthis.throwable = throwable;\n\t\t\n\t\tStackTraceElement element = throwable.getStackTrace()[0];\n\t\tthis.className = element.getClassName();\n\t\tthis.methodName = element.getMethodName();\n\t\tthis.fileName = element.getFileName();\n\t\tthis.lineNumber = element.getLineNumber();\n\t\t\n\t\tif (throwable instanceof BusinessException) {\n\t\t\tBusinessException business = (BusinessException) throwable;\n\t\t\tthis.code = business.getErrorCode();\n\t\t\tthis.info = business.getInfoStack();\n\t\t} else {\n\t\t\tthis.code = null;\n\t\t\tthis.info = null;\n\t\t}\n\t}\n\n\tpublic Throwable getThrowable() {\n\t\treturn throwable;\n\t}\n\n\tpublic ErrorReport getCause() {\n\t\treturn cause;\n\t}\n\n\tpublic void setCause(ErrorReport cause) {\n\t\tthis.cause = cause;\n\t}\n\n\tpublic String getClassName() {\n\t\treturn className;\n\t}\n\n\tpublic String getMethodName() {\n\t\treturn methodName;\n\t}\n\n\tpublic String getFileName() {\n\t\treturn fileName;\n\t}\n\n\tpublic int getLineNumber() {\n\t\treturn lineNumber;\n\t}\n\t\n\tpublic ErrorCode getCode() {\n\t\treturn code;\n\t}\n\n\tpublic RichProperties getInfo() {\n\t\tif (info == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn info.peek();\n\t}\n\t\n\tpublic String toString() {\n\t\tStringBuilder sb = new StringBuilder();\n\t\ttoString(sb, this);\n\t\treturn sb.toString();\n\t}\n\t\n\tprivate void toString(StringBuilder sb, RichProperties info, boolean hasMessage) {\n\t\tif (hasMessage) {\n\t\t\tsb.append(info.getMessage()).append(N);\n\t\t}\n\t\tIterator<String> keys = info.iterator();\n\t\twhile (keys.hasNext()) {\n\t\t\tString key = keys.next();\n\t\t\tObject value = info.get(key);\n\t\t\tsb.append(INDENT).append(key).append(COLON);\n\t\t\tString valueText = Strings.toString(value);\n\t\t\tsb.append(valueText).append(N);\n\t\t}\n\t}\n\t\n\tprivate void toString(StringBuilder sb, ErrorReport report) {\n\t\t\n\t\t// 异常在代码中抛出的位置。\n\t\tsb.append(report.className).append(\".\").append(report.methodName).append(\"(\")\n\t\t\t\t.append(report.fileName).append(COLON).append(report.lineNumber).append(\")\").append(N);\n\t\t\n\t\t// 异常描述。\n\t\tsb.append(\"Error Description\").append(COLON).append(report.getThrowable().getMessage()).append(N);\n\t\t\n\t\t// 异常码\n\t\tErrorCode code = report.code;\n\t\tif (code != null) {\n\t\t\tsb.append(INDENT).append(\"errorCode\").append(COLON).append(code.getValue()).append(N)\n\t\t\t\t\t.append(INDENT).append(\"errorName\").append(COLON).append(code.getName()).append(N);\n\t\t}\n\t\t\t\t\n\t\t// 异常抛出时的上下文数据信息\n\t\tRichProperties topInfo = report.getInfo();\n\t\tif (topInfo != null && topInfo.size() > 0) {\n\t\t\ttoString(sb, topInfo, false);\n\t\t}\n\t\tStack<RichProperties> infoStack = report.info;\n\t\tif (infoStack != null && infoStack.size() > 1) {\n\t\t\tfor (int i = infoStack.size() - 2; i >= 0; i--) {\n\t\t\t\tRichProperties info = infoStack.get(i);\n\t\t\t\tsb.append(\"cause by\").append(COLON);\n\t\t\t\ttoString(sb, info, true);\n\t\t\t}\n\t\t}\n\t\t\n\t\t// 异常堆栈。\n\t\tThrowable throwable = report.throwable;\n\t\tif (!(throwable instanceof BusinessException) || report.cause == null) {\n\t\t\tsb.append(Strings.getString(throwable)).append(N);\n\t\t}\n\t\t\n\t\t// 异常根源（上一个异常）\n\t\tErrorReport cause = report.cause;\n\t\tif (cause != null) {\n\t\t\tsb.append(\"cause by\").append(COLON);\n\t\t\ttoString(sb, cause);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/ResourceErrorCode.java",
    "content": "package com.terran4j.commons.util.error;\n\nimport java.io.IOException;\nimport java.util.Locale;\n\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.value.ResourceBundlesProperties;\n\npublic class ResourceErrorCode implements ErrorCode {\n\t\n\tprivate final int value;\n\t\n\tprivate final String name;\n\t\n\tprivate final String message;\n\t\n\tpublic ResourceErrorCode(String name, Locale locale) {\n\t\tsuper();\n\t\tif(Strings.isNull(name))name = \"\";\n\t\tthis.value = Math.abs(name.hashCode());\n\t\tthis.name = name;\n\t\t\n\t\tResourceBundlesProperties props = null;\n\t\ttry {\n\t\t\tprops = ResourceBundlesProperties.get(\"error\", locale);\n\t\t} catch (IOException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t\tthis.message = props == null ? null : props.get(name);\n\t}\n\n\t@Override\n\tpublic int getValue() {\n\t\treturn value;\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\t@Override\n\tpublic String getMessage() {\n\t\treturn message;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/SimpleErrorCode.java",
    "content": "package com.terran4j.commons.util.error;\n\npublic class SimpleErrorCode implements ErrorCode {\n\t\n\tpublic static final ErrorCode UNKNOW = new SimpleErrorCode(0, \"unknow.error\");\n\n\tprivate final int value;\n\n\tprivate final String name;\n\n\tpublic SimpleErrorCode(int value, String name) {\n\t\tsuper();\n\t\tthis.value = value;\n\t\tthis.name = name;\n\t}\n\n\t@Override\n\tpublic int getValue() {\n\t\treturn this.value;\n\t}\n\n\t@Override\n\tpublic String getName() {\n\t\treturn this.name;\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/error/null.properties",
    "content": "\n# null resource bundle."
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/reflect/InterfaceFilter.java",
    "content": "package com.terran4j.commons.util.reflect;\n\nimport com.terran4j.commons.util.Classes;\nimport org.springframework.core.type.ClassMetadata;\nimport org.springframework.core.type.classreading.MetadataReader;\nimport org.springframework.core.type.classreading.MetadataReaderFactory;\nimport org.springframework.core.type.filter.TypeFilter;\n\nimport java.io.IOException;\nimport java.security.InvalidParameterException;\n\n/**\n * 判断接口或父接口是否有指定接口的过滤器。\n */\npublic class InterfaceFilter implements TypeFilter {\n\n    private final Class<?> interfaceClass;\n\n    public InterfaceFilter(Class<?> interfaceClass) {\n        super();\n        if (interfaceClass == null || !interfaceClass.isInterface()) {\n            throw new InvalidParameterException(interfaceClass\n                    + \" is NOT an interface class.\");\n        }\n        this.interfaceClass = interfaceClass;\n    }\n\n    @Override\n    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {\n        ClassMetadata classMeta = metadataReader.getClassMetadata();\n        if (!classMeta.isInterface()) {\n            return false;\n        }\n        String[] interfaceNames = classMeta.getInterfaceNames();\n        if (interfaceNames == null || interfaceNames.length == 0) {\n            return false;\n        }\n        for (String interfaceName : interfaceNames) {\n            if (isContainInterface(interfaceName)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private boolean isContainInterface(String interfaceName) {\n        if (interfaceClass.getName().equals(interfaceName)) {\n            return true;\n        }\n\n        Class<?> currentClass = null;\n        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();\n        try {\n            currentClass = classLoader.loadClass(interfaceName);\n        } catch (ClassNotFoundException e) {\n            throw new RuntimeException(e);\n        }\n        return Classes.isInterfaceExtends(currentClass, interfaceClass);\n    }\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/security/AsymmetricKeys.java",
    "content": "package com.terran4j.commons.util.security;\n\nimport java.security.InvalidKeyException;\nimport java.security.KeyFactory;\nimport java.security.KeyPair;\nimport java.security.KeyPairGenerator;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.security.interfaces.RSAPrivateKey;\nimport java.security.interfaces.RSAPublicKey;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\n\nimport javax.crypto.BadPaddingException;\nimport javax.crypto.Cipher;\nimport javax.crypto.IllegalBlockSizeException;\nimport javax.crypto.NoSuchPaddingException;\n\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.bouncycastle.util.encoders.Base64;\n\nimport com.terran4j.commons.util.Encoding;\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\n\n/**\n * 非对称密钥。\n * \n * @author wei.jiang\n *\n */\npublic class AsymmetricKeys {\n\t\n\tprivate static final int KEY_LENGTH = 512;\n\t\n\tprivate static final String ALGORITHM_RSA = \"RSA\";\n\t\n\tprivate static volatile KeyFactory keyFactory = null;\n\t\n\tprivate static volatile KeyPairGenerator keyPairGen = null;\n\t\n\tprivate static final KeyFactory getKeyFactory() throws BusinessException {\n\t\tif (keyFactory != null) {\n\t\t\treturn keyFactory;\n\t\t}\n\t\tsynchronized (AsymmetricKeys.class) {\n\t\t\tif (keyFactory != null) {\n\t\t\t\treturn keyFactory;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tkeyFactory = KeyFactory.getInstance(ALGORITHM_RSA);\n\t\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t\t.put(\"algorithm\", ALGORITHM_RSA)\n\t\t\t\t\t\t.setMessage(\"No Such Algorithm: ${algorithm}\");\n\t\t\t}\n\t\t\treturn keyFactory;\n\t\t}\n\t}\n\t\n\tprivate static final KeyPairGenerator getKeyPairGenerator() throws BusinessException {\n\t\tif (keyPairGen != null) {\n\t\t\treturn keyPairGen;\n\t\t}\n\t\tsynchronized (AsymmetricKeys.class) {\n\t\t\tif (keyPairGen != null) {\n\t\t\t\treturn keyPairGen;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tkeyPairGen = KeyPairGenerator.getInstance(ALGORITHM_RSA);\n\t\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t\t.put(\"algorithm\", ALGORITHM_RSA)\n\t\t\t\t\t\t.setMessage(\"No Such Algorithm: ${algorithm}\");\n\t\t\t}\n\t\t\tkeyPairGen.initialize(KEY_LENGTH, new SecureRandom());\n\t\t\treturn keyPairGen;\n\t\t}\n\t}\n\t\n\t/**\n\t * 公钥\n\t */\n\tprivate RSAPublicKey publicKey;\n\t\n\t/**\n\t * 私钥\n\t */\n\tprivate RSAPrivateKey privateKey;\n\n\t/**\n\t * 公钥加密器。\n\t */\n\tprivate Cipher publicCipher;\n\t\n\t/**\n\t * 私钥解密器。\n\t */\n\tprivate Cipher privateCipher;\n\t\n\t/**\n\t * 随机生成密钥对\n\t * @throws BusinessException 生成出错。\n\t */\n\tpublic AsymmetricKeys() throws BusinessException {\n\t\tsuper();\n\t\tKeyPair keyPair = getKeyPairGenerator().generateKeyPair();\n\t\tthis.publicKey = (RSAPublicKey) keyPair.getPublic();\n\t\tthis.privateKey = (RSAPrivateKey) keyPair.getPrivate();\n\t\tthis.publicCipher = initCipher(publicKey);\n\t\tthis.privateCipher = initCipher(privateKey);\n\t}\n\t\n\tpublic AsymmetricKeys(String publicKeyText, String privateKeyText) throws BusinessException {\n\t\tsuper();\n\t\tthis.publicKey = loadPublicKey(publicKeyText);\n\t\tthis.privateKey = loadPrivateKey(privateKeyText);\n\t\tthis.publicCipher = initCipher(publicKey);\n\t\tthis.privateCipher = initCipher(privateKey);\n\t}\n\t\n\tprivate String encode(byte[] key) {\n\t\treturn Base64.toBase64String(key);\n\t}\n\t\n\tprivate byte[] decode(String key) {\n\t\treturn Base64.decode(key);\n\t}\n\t\n\tpublic String getPublicKey() {\n\t\treturn encode(publicKey.getEncoded());\n\t}\n\t\n\tpublic String getPrivateKey() {\n\t\treturn encode(privateKey.getEncoded());\n\t}\n\t\n\tprivate Cipher initCipher(RSAPrivateKey privateKey) throws BusinessException {\n\t\ttry {\n\t\t\tCipher cipher = Cipher.getInstance(ALGORITHM_RSA, new BouncyCastleProvider());\n\t\t\tcipher.init(Cipher.DECRYPT_MODE, privateKey);\n\t\t\treturn cipher;\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t.put(\"algorithm\", ALGORITHM_RSA)\n\t\t\t\t\t.setMessage(\"No Such Algorithm: ${algorithm}\");\n\t\t} catch (NoSuchPaddingException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t.setMessage(\"No Such Padding\");\n\t\t} catch (InvalidKeyException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t.setMessage(\"解密私钥非法,请检查\");\n\t\t}\n\t}\n\t\n\tprivate Cipher initCipher(RSAPublicKey publicKey) throws BusinessException {\n\t\ttry {\n\t\t\tCipher cipher = Cipher.getInstance(ALGORITHM_RSA, new BouncyCastleProvider());\n\t\t\tcipher.init(Cipher.ENCRYPT_MODE, publicKey);\n\t\t\treturn cipher;\n\t\t} catch (NoSuchAlgorithmException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t.put(\"algorithm\", ALGORITHM_RSA)\n\t\t\t\t\t.setMessage(\"No Such Algorithm: ${algorithm}\");\n\t\t} catch (NoSuchPaddingException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t.setMessage(\"No Such Padding\");\n\t\t} catch (InvalidKeyException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INTERNAL_ERROR, e)\n\t\t\t\t\t.setMessage(\"加密公私非法,请检查\");\n\t\t}\n\t}\n\t\n\t/**\n\t * 从字符串中加载公钥\n\t * \n\t * @param publicKeyText\n\t *            公钥数据字符串\n\t * @throws Exception\n\t *             加载公钥时产生的异常\n\t */\n\tprivate RSAPublicKey loadPublicKey(String publicKeyText) throws BusinessException {\n\t\ttry {\n\t\t\tbyte[] data = decode(publicKeyText);\n\t\t\tX509EncodedKeySpec keySpec = new X509EncodedKeySpec(data);\n\t\t\treturn (RSAPublicKey) getKeyFactory().generatePublic(keySpec);\n\t\t} catch (InvalidKeySpecException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INVALID_PARAM, e)\n\t\t\t\t\t.put(\"publicKey\", publicKeyText)\n\t\t\t\t\t.setMessage(\"公钥非法\");\n\t\t}\n\t}\n\n\tprivate RSAPrivateKey loadPrivateKey(String privateKeyText) throws BusinessException {\n\t\ttry {\n\t\t\tbyte[] data = decode(privateKeyText);\n\t\t\tPKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(data);\n\t\t\treturn (RSAPrivateKey) getKeyFactory().generatePrivate(keySpec);\n\t\t} catch (InvalidKeySpecException e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INVALID_PARAM, e)\n\t\t\t\t\t.put(\"privateKey\", privateKeyText)\n\t\t\t\t\t.setMessage(\"私钥非法\");\n\t\t}\n\t}\n\n\tprivate byte[] encrypt(byte[] data) throws Exception {\n\t\ttry {\n\t\t\tbyte[] output = publicCipher.doFinal(data);\n\t\t\treturn output;\n\t\t} catch (IllegalBlockSizeException e) {\n\t\t\tthrow new Exception(\"明文长度非法\", e);\n\t\t} catch (BadPaddingException e) {\n\t\t\tthrow new Exception(\"明文数据已损坏\", e);\n\t\t}\n\t}\n\t\n\t/**\n\t * 用公钥加密\n\t * @param plainText 明文\n\t * @return 密文\n\t * @throws BusinessException 加密过程中的异常信息\n\t */\n\tpublic String encrypt(String plainText) throws BusinessException {\n\t\ttry {\n\t\t\tbyte[] data = plainText.getBytes(Encoding.UTF8.getName());\n\t\t\tbyte[] encryptedData = encrypt(data);\n\t\t\tString result = Strings.toHexString(encryptedData);\n\t\t\treturn result;\n\t\t} catch (Exception e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INVALID_PARAM, e)\n\t\t\t\t\t.put(\"plainText\", plainText)\n\t\t\t\t\t.setMessage(\"非法的明文数据\");\n\t\t}\n\t}\n\t\n\t/**\n\t * 用私钥解密\n\t * @param cipherText 密文\n\t * @return 明文\n\t * @throws BusinessException 解密过程中的异常信息\n\t */\n\tpublic String decrypt(String cipherText) throws BusinessException {\n\t\ttry {\n\t\t\tbyte[] data = Strings.fromHexString(cipherText);\n\t\t\tbyte[] decryptedData = decrypt(data);\n\t\t\tString result = new String(decryptedData, Encoding.UTF8.getName());\n\t\t\treturn result;\n\t\t} catch (Exception e) {\n\t\t\tthrow new BusinessException(CommonErrorCode.INVALID_PARAM, e)\n\t\t\t\t\t.put(\"cipherText\", cipherText)\n\t\t\t\t\t.setMessage(\"非法的密文\");\n\t\t}\n\t}\n\n\tprivate byte[] decrypt(byte[] data) throws Exception {\n\t\tif (privateKey == null) {\n\t\t\tthrow new Exception(\"解密私钥为空, 请设置\");\n\t\t}\n\t\ttry {\n\t\t\tbyte[] output = privateCipher.doFinal(data);\n\t\t\treturn output;\n\t\t} catch (IllegalBlockSizeException e) {\n\t\t\tthrow new Exception(\"密文长度非法\", e);\n\t\t} catch (BadPaddingException e) {\n\t\t\tthrow new Exception(\"密文数据已损坏\", e);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/security/MD5Util.java",
    "content": "package com.terran4j.commons.util.security;\n\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.CommonErrorCode;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.util.StringUtils;\n\nimport java.io.UnsupportedEncodingException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Arrays;\nimport java.util.Map;\n\n@Slf4j\npublic class MD5Util {\n\n    public static final String ALGORITHM_MD5 = \"MD5\";\n\n    private static MessageDigest md = null;\n\n    private static final MessageDigest getMessageDigest() throws BusinessException {\n        if (md != null) {\n            return md;\n        }\n        synchronized (MD5Util.class) {\n            if (md != null) {\n                return md;\n            }\n            try {\n                md = MessageDigest.getInstance(ALGORITHM_MD5);\n                return md;\n            } catch (NoSuchAlgorithmException e) {\n                log.error(\"NoSuchAlgorithmException: \" + e.getMessage(), e);\n                throw new BusinessException(CommonErrorCode.INTERNAL_ERROR, e).put(\"algorithm\", ALGORITHM_MD5)\n                        .setMessage(\"No Such Algorithm: ${algorithm}\");\n            }\n        }\n    }\n\n    /**\n     * 给一组数据计算签名。<br>\n     * 签名生成的通用步骤如下：<br>\n     * 第一步，设所有发送或者接收到的数据为集合M，将集合M内非空参数值的参数\n     * 按照参数名ASCII码从小到大排序（字典序），使用URL键值对的\n     * 格式（即key1=value1&key2=value2…）拼接成字符串stringA。<br>\n     * 特别注意以下重要规则：<br>\n     * <li>参数名ASCII码从小到大排序（字典序）；</li>\n     * <li>如果参数的值为空不参与签名；</li>\n     * <li>参数名区分大小写；</li>\n     * <li>验证调用返回或微信主动通知签名时，传送的sign参数不参与签名，\n     * 将生成的签名与该sign值作校验。</li>\n     * 第二步，在 stringA 最后拼接上 key 得到 stringSignTemp 字符串，\n     * 并对 stringSignTemp 进行MD5运算，再将得到的字符串所有字符转换为大写，\n     * 得到 sign 值 signValue 。\n     *\n     * @param data      数据。\n     * @param secretKey 密钥。\n     * @return 签名串。\n     * @throws BusinessException 签名出错。\n     */\n    public static String signature(Map<String, String> data, String secretKey) throws BusinessException {\n\n        StringBuilder sb = new StringBuilder();\n\n        Object[] keys = data.keySet().toArray();\n        Arrays.sort(keys);\n        for (Object key : keys) {\n            String value = data.get(key);\n            if (!StringUtils.isEmpty(value)) {\n                sb.append(key + \"=\" + value + \"&\");\n            }\n        }\n\n        String stringA = \"\";\n        if (sb.length() > 0) {\n            stringA += sb.substring(0, sb.length() - 1);\n        }\n\n        String stringSignTemp = stringA + secretKey;\n        log.info(\"MD5 signature, stringSignTemp: {}\", stringSignTemp);\n        String signValue = md5(stringSignTemp).toUpperCase();\n        return signValue;\n    }\n\n    public static String md5(String text) throws BusinessException {\n        try {\n            byte[] input = text.getBytes(\"UTF-8\");\n            byte[] data = getMessageDigest().digest(input);\n            StringBuffer sb = new StringBuffer();\n            for (int i = 0; i < data.length; i++) {\n                sb.append(Integer.toString((data[i] & 0xff) + 0x100, 16).substring(1));\n            }\n            return sb.toString();\n        } catch (BusinessException e) {\n            throw e.put(\"text\", text);\n        } catch (UnsupportedEncodingException e) {\n            throw new BusinessException(ErrorCodes.INTERNAL_ERROR)\n                    .put(\"text\", text).setMessage(\"输入文本不是UTF-8格式的\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/security/Security.java",
    "content": "package com.terran4j.commons.util.security;\n\nimport java.util.Map;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport com.terran4j.commons.util.error.BusinessException;\n\npublic class Security {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(Security.class);\n\n\t/**\n\t * 创建一对非对称密钥。\n\t * @return 非对称密钥。\n\t * @throws BusinessException 创建出错。\n\t */\n\tpublic static final AsymmetricKeys createAsymmetricKeys() throws BusinessException {\n\t\treturn new AsymmetricKeys();\n\t}\n\t\n\tpublic static final AsymmetricKeys buildAsymmetricKeys(String publicKey, String privateKey) throws BusinessException {\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"build AsymmetricKeys, publicKey = {}, privateKey = {}\", publicKey, privateKey);\n\t\t}\n\t\treturn new AsymmetricKeys(publicKey, privateKey);\n\t}\n\t\n\tpublic static String signature(Map<String, String> data, String secretKey) throws BusinessException {\n\t\treturn MD5Util.signature(data, secretKey);\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/task/LoopExecuteTask.java",
    "content": "package com.terran4j.commons.util.task;\n\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * 搭建一个永续循环的任务框架，帮助控制循环的节奏、定期打印日志输出报告任务执行情况。\n * @author wei.jiang\n *\n */\n@Slf4j\npublic abstract class LoopExecuteTask implements Runnable {\n\n\tprivate long sleepTime = 1000;\n\n\tprivate long reportIntervalSecond = 60;\n\n\tprivate long executeCount;\n\n\tprivate long failedCount;\n\n\tprivate Thread thread;\n\n\tprivate volatile boolean running = false;\n\n\tpublic LoopExecuteTask() {\n\t\tthis(0);\n\t}\n\n\tpublic LoopExecuteTask(long sleepTime) {\n\t\tsuper();\n\t\tthis.sleepTime = sleepTime;\n\t}\n\n\tpublic final long getSleepTime() {\n\t\treturn sleepTime;\n\t}\n\n\tpublic long getReportIntervalSecond() {\n\t\treturn reportIntervalSecond;\n\t}\n\n\tpublic long getFailedCount() {\n\t\treturn failedCount;\n\t}\n\n\tpublic LoopExecuteTask setReportIntervalSecond(long reportIntervalSecond) {\n\t\tthis.reportIntervalSecond = reportIntervalSecond;\n\t\treturn this;\n\t}\n\n\tpublic long getExecuteCount() {\n        return executeCount;\n    }\n\n    public Thread getThread() {\n        return thread;\n    }\n\n    public boolean isRunning() {\n        return running;\n    }\n\n    public void stop() {\n        thread.interrupt();\n    }\n\n    public final LoopExecuteTask setSleepTime(long sleepTime) {\n        this.sleepTime = sleepTime;\n        return this;\n    }\n\n    @Override\n    public final void run() {\n        if (this.thread != null) {\n            throw new IllegalStateException(\"LoopExecuteTask Object can't be running in multi-thread.\");\n\t\t}\n\n\t\tonStart();\n\t\t\n\t\tthread = Thread.currentThread();\n\t\tString threadName = thread.getName();\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"{} is starting...\", threadName);\n\t\t}\n\n\t\texecuteCount = 0;\n\t\tfailedCount = 0;\n\t\tlong totalSpendTime = 0;\n\t\tlong lastReportTime = System.currentTimeMillis();\n\t\trunning = true;\n\t\tlong retrySleepTime = 100;\n\t\twhile (!thread.isInterrupted()) {\n\t\t\texecuteCount++;\n\t\t\ttry {\n\t\t\t\tlong t0 = System.currentTimeMillis();\n\t\t\t\tboolean willContinue = execute();\n\t\t\t\tlong t = System.currentTimeMillis() - t0;\n\t\t\t\ttotalSpendTime += t;\n\t\t\t\tif (!willContinue) {\n\t\t\t\t\tsleep();\n\t\t\t\t}\n\t\t\t\tretrySleepTime = 100;\n\t\t\t} catch (Exception e) {\n\t\t\t\tfailedCount++;\n\t\t\t\tboolean willContinue = handle(e);\n\t\t\t\tif (!willContinue) {\n\t\t\t\t\tbreak;\n\t\t\t\t} else { // 失败后的重试，间隔时间应该是递增的。\n\t\t\t\t\tretrySleepTime = retrySleepTime * 2;\n\t\t\t\t\tif (retrySleepTime > sleepTime) {\n\t\t\t\t\t\tretrySleepTime = sleepTime;\n\t\t\t\t\t}\n\t\t\t\t\tsleep(retrySleepTime);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlong t = System.currentTimeMillis() - lastReportTime;\n\t\t\tif (t > reportIntervalSecond * 1000) {\n\t\t\t\tif (log.isInfoEnabled()) {\n\t\t\t\t\tlog.info(\n\t\t\t\t\t\t\t\"{} is running..., executeCount = {}, failedCount = {}, totalSpendTime = {}, avgSpendTime = {}\",\n\t\t\t\t\t\t\tthreadName, executeCount, failedCount, totalSpendTime, totalSpendTime / executeCount);\n\t\t\t\t}\n\t\t\t\tlastReportTime = System.currentTimeMillis();\n\t\t\t}\n\t\t}\n\t\t\n\t\trunning = false;\n\t\tif (log.isInfoEnabled()) {\n\t\t\tlog.info(\"{} is stopped...\", threadName);\n\t\t}\n\t\tthread = null;\n\t\t\n\t\tonStop();\n\t}\n\n\tprotected final void sleep() {\n\t\ttry {\n\t\t\tThread.sleep(sleepTime);\n\t\t} catch (InterruptedException e) {\n\t\t\tif (log.isWarnEnabled()) {\n\t\t\t\tlog.warn(\"{} is Interrupted.\", thread.getName());\n\t\t\t}\n\t\t}\n\t}\n\t\n\tprotected final void sleep(long sleepTime) {\n\t\ttry {\n\t\t\tThread.sleep(sleepTime);\n\t\t} catch (InterruptedException e) {\n\t\t\tif (log.isWarnEnabled()) {\n\t\t\t\tlog.warn(\"{} is Interrupted.\", thread.getName());\n\t\t\t}\n\t\t}\n\t}\n\n\tprotected boolean handle(Exception e) {\n\t\tString threadName = thread.getName();\n\t\tlog.error(\"{} execute occur Exception[{}], cause by {}\", threadName, e.getClass().getName(), e.getMessage(), e);\n\t\treturn true;\n\t}\n\n\t/**\n\t * 执行操作（一次循环里面的内容）。\n\t * @return 是否继续循环执行， true表示继续下一次的循环执行，\n\t * false表示sleep一段时间再进入下一次的循环执行\n\t * @throws Exception 执行出错。\n\t */\n\tprotected abstract boolean execute() throws Exception;\n\t\n\tprotected void onStart() {\n\t}\n\t\n\tprotected void onStop() {\n\t}\n\n}"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/value/JsonValueSource.java",
    "content": "package com.terran4j.commons.util.value;\n\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\n\npublic class JsonValueSource implements ValueSource<String, String> {\n\t\n\tprivate final JsonObject result;\n\t\n\tpublic JsonValueSource(JsonObject result) {\n\t\tsuper();\n\t\tthis.result = result;\n\t}\n\t\n\tpublic JsonObject getSource() {\n\t\treturn this.result;\n\t}\n\n\t@Override\n\tpublic String get(String key) {\n\t\tif (result == null) {\n\t\t\treturn null;\n\t\t}\n\t\tString[] array = key.split(\"\\\\.\");\n\t\tJsonElement current = result;\n\t\tfor (String item : array) {\n\t\t\tJsonElement element = current.getAsJsonObject().get(item);\n\t\t\tif (element == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tcurrent = element;\n\t\t}\n\t\treturn current.getAsJsonPrimitive().getAsString();\n\t}\n\t\n\tpublic JsonElement getElement(String key) {\n\t\tif (result == null) {\n\t\t\treturn null;\n\t\t}\n\t\tString[] array = key.split(\"\\\\.\");\n\t\tJsonElement current = result;\n\t\tfor (String item : array) {\n\t\t\tJsonElement element = current.getAsJsonObject().get(item);\n\t\t\tif (element == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tcurrent = element;\n\t\t}\n\t\treturn current;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/value/KeyedList.java",
    "content": "package com.terran4j.commons.util.value;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * 带索引的列表对象，其特点如下：<br>\n * 1. 插入修改快速，删除慢。<br>\n * 2. 既保持了列表的有序性，也能根据一个key快速找到对应的value。<br>\n * 3. 本类是非线性安全的，请使用者自行保证它的线性安全性。<br>\n * 4. 不允许容纳重复的元素。\n */\npublic final class KeyedList<K, V> implements ValueSource<K, V> {\n\n\tprivate final List<K> list = new ArrayList<K>();\n\n\tprivate final Map<K, V> map = new HashMap<K, V>();\n\n\tpublic KeyedList() {\n\t\tsuper();\n\t}\n\n\t@Override\n\tpublic V get(K key) {\n\t\treturn map.get(key);\n\t}\n\n\tpublic void clear() {\n\t\tlist.clear();\n\t\tmap.clear();\n\t}\n\n\tpublic boolean containsKey(K key) {\n\t\treturn map.containsKey(key);\n\t}\n\n\tpublic int getIndex(K key) {\n\t\tif (key == null) {\n\t\t\treturn -1;\n\t\t}\n\t\treturn list.indexOf(key);\n\t}\n\n\tprivate void checkNotNull(K key) {\n\t\tif (key == null) {\n\t\t\tthrow new NullPointerException(\"key is null.\");\n\t\t}\n\t}\n\n\tpublic void add(K key, V value, int index) {\n\t\tcheckNotNull(key);\n\t\tif (containsKey(key)) {\n\t\t\tthrow new RuntimeException(\"key[\" + key + \"] already existed of value: \" + value);\n\t\t}\n\t\tmap.put(key, value);\n\t\tlist.add(index, key);\n\t}\n\n\tpublic void addOrUpdate(K key, V value) {\n\t\tcheckNotNull(key);\n\t\tif (containsKey(key)) {\n\t\t\tmap.put(key, value);\n\t\t} else {\n\t\t\tadd(key, value, size());\n\t\t}\n\t}\n\n\tpublic void add(K key, V value) {\n\t\tadd(key, value, size());\n\t}\n\n\tpublic V getByKey(K key) {\n\t\tif (key == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn map.get(key);\n\t}\n\n\tpublic V removeByKey(K key) {\n\t\tif (key == null) {\n\t\t\treturn null;\n\t\t}\n\t\tV value = map.remove(key);\n\t\tlist.remove(key);\n\t\treturn value;\n\t}\n\n\tpublic int size() {\n\t\treturn list.size();\n\t}\n\n\tpublic V get(int index) {\n\t\tif (index < 0 || index >= list.size()) {\n\t\t\treturn null;\n\t\t}\n\n\t\tK key = list.get(index);\n\t\tif (key == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn map.get(key);\n\t}\n\n\tpublic K getKey(int index) {\n\t\treturn list.get(index);\n\t}\n\n\tpublic V remove(int index) {\n\t\tK key = getKey(index);\n\t\tif (key != null) {\n\t\t\tlist.remove(index);\n\t\t\treturn map.remove(key);\n\t\t}\n\t\treturn null;\n\t}\n\n\tpublic Set<K> keySet() {\n\t\treturn map.keySet();\n\t}\n\n\tpublic List<V> getAll() {\n\t\tList<V> all = new ArrayList<V>();\n\t\tfor (K key : list) {\n\t\t\tV item = map.get(key);\n\t\t\tif (item != null) {\n                all.add(item);\n            }\n\t\t}\n\t\treturn all;\n\t}\n\n\t@Override\n\tpublic KeyedList<K, V> clone() {\n\t\tKeyedList<K, V> copy = new KeyedList<K, V>();\n\t\tfor (K key : list) {\n\t\t\tcopy.list.add(key);\n\t\t\tcopy.map.put(key, map.get(key));\n\t\t}\n\t\treturn copy;\n\t}\n\n\tpublic KeyedList<K, V> deduct(KeyedList<K, V> other) {\n\t\tif (other == null || other.size() == 0) {\n\t\t\treturn clone();\n\t\t}\n\n\t\tKeyedList<K, V> copy = new KeyedList<K, V>();\n\t\tfor (K key : list) {\n\t\t\tif (!other.containsKey(key)) {\n\t\t\t\tcopy.add(key, getByKey(key));\n\t\t\t}\n\t\t}\n\t\treturn copy;\n\t}\n\n\tpublic String toString() {\n\t\tStringBuffer sb = new StringBuffer();\n\t\tfor (K key : list) {\n\t\t\tsb.append(key).append(\" = \").append(map.get(key)).append(\"\\n\");\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/value/MapValueSource.java",
    "content": "package com.terran4j.commons.util.value;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class MapValueSource<K, V> implements ValueSource<K, V> {\n\n\tprivate final Map<K, V> map;\n\n\tpublic MapValueSource() {\n\t\tthis(new HashMap<>());\n\t}\n\n\tpublic MapValueSource(Map<K, V> map) {\n\t\tsuper();\n\t\tthis.map = map;\n\t}\n\n\tpublic MapValueSource<K, V> put(K key, V value) {\n\t\tmap.put(key, value);\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic V get(K key) {\n\t\treturn map.get(key);\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/value/ResourceBundlesProperties.java",
    "content": "package com.terran4j.commons.util.value;\n\nimport com.terran4j.commons.util.Strings;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.URL;\nimport java.util.Enumeration;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * 可以从类文件中获取多个相同路径的资源文件，合并后形成一个 Properties 。\n * \n * @author jiangwei\n *\n */\npublic class ResourceBundlesProperties implements ValueSource<String, String> {\n\n\tprivate static final Map<String, ResourceBundlesProperties> cache = new ConcurrentHashMap<>();\n\n\tpublic static final ResourceBundlesProperties get(String path, Locale locale) throws IOException {\n\t\tif (path == null) {\n\t\t\tthrow new NullPointerException();\n\t\t}\n\t\tif (locale == null) {\n\t\t\tlocale = Locale.getDefault();\n\t\t}\n\t\t\n\t\tString cacheKey = path + \"_\" + locale.getLanguage() + \"_\" + locale.getCountry();\n\t\tif(Strings.isNull(locale.getCountry())){\n\t\t\tcacheKey = path + \"_\" + locale.getLanguage();\n\t\t}\n\t\tResourceBundlesProperties props = cache.get(cacheKey);\n\t\tif (props != null) {\n\t\t\treturn props;\n\t\t}\n\n\t\tsynchronized (ResourceBundlesProperties.class) {\n\t\t\tprops = cache.get(cacheKey);\n\t\t\tif (props != null) {\n\t\t\t\treturn props;\n\t\t\t}\n\n\t\t\tProperties srcProps = load(path, locale);\n\t\t\tprops = new ResourceBundlesProperties(srcProps);\n\t\t\tcache.put(cacheKey, props);\n\n\t\t\treturn props;\n\t\t}\n\t}\n\t\n\tprivate static final Properties load(String path, Locale locale) throws IOException {\n\t\tString defaultPath = path + \".properties\";\n\t\tProperties props = load(defaultPath);\n\t\tif (props == null) {\n\t\t\tprops = new Properties();\n\t\t}\n\t\t\n\t\tString fixedPath = path + \"_\" + locale.getLanguage() + \"_\" + locale.getCountry() + \".properties\";\n\t\tif(Strings.isNull(locale.getCountry())){\n\t\t\tfixedPath = path + \"_\" + locale.getLanguage() + \".properties\";\n\t\t}\n\t\tProperties fixedProps = load(fixedPath);\n\t\tif (fixedProps != null) {\n\t\t\tprops.putAll(fixedProps);\n\t\t}\n\t\t\n\t\treturn props;\n\t}\n\n\tprivate static final Properties load(String path) throws IOException {\n\n\t\tEnumeration<URL> urls = ResourceBundlesProperties.class.getClassLoader().getResources(path);\n\t\tif (urls == null || !urls.hasMoreElements()) {\n\t\t\treturn null;\n\t\t}\n\n\t\tProperties props = new Properties();\n\t\t\n\t\twhile (urls.hasMoreElements()) {\n\t\t\tURL url = urls.nextElement();\n\t\t\tInputStream in = url.openStream();\n\t\t\tif (in != null) {\n\t\t\t\ttry {\n\t\t\t\t\tprops.load(new InputStreamReader(in, \"UTF-8\"));\n\t\t\t\t} finally {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tin.close();\n\t\t\t\t\t} catch (Exception e1) {\n\t\t\t\t\t\t// ignore.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn props;\n\t}\n\n\tprivate final Properties props;\n\n\tpublic ResourceBundlesProperties(Properties props) {\n\t\tsuper();\n\t\tthis.props = props;\n\t}\n\n\t@Override\n\tpublic String get(String key) {\n\t\tif (key == null || props == null) {\n\t\t\treturn null;\n\t\t}\n\t\treturn props.getProperty(key);\n\t}\n\n\t/**\n\t * 支持 {} 形式的参数\n\t * @param key\n\t * @param args\n\t * @return\n\t */\n\tpublic String get(String key, Object... args) {\n\t\tif (key == null || props == null) {\n\t\t\treturn null;\n\t\t}\n\t\tString value = props.getProperty(key);\n\t\tif(args == null)return value;\n\n\t\tfor(Object arg : args){\n\t\t\tvalue =value.replace(\"{}\", arg.toString());\n\t\t}\n\n\t\treturn value;\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/value/RichProperties.java",
    "content": "package com.terran4j.commons.util.value;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport com.terran4j.commons.util.Strings;\n\npublic final class RichProperties implements ValueSource<String, String>{\n\t\n\tprivate static final Object NULL = new Object();\n\n\tprivate final Map<String, Object> props = new ConcurrentHashMap<String, Object>();\n\n\tprivate String message;\n\t\n\tpublic RichProperties setMessage(String message) {\n\t\tthis.message = message;\n\t\treturn this;\n\t}\n\n\tpublic RichProperties() {\n\t\tsuper();\n\t}\n\n\tpublic RichProperties(String message) {\n\t\tsuper();\n\t\tthis.message = message;\n\t}\n\n\t/**\n\t * @return the message\n\t */\n\tpublic final String getMessage() {\n\t\treturn Strings.format(message, this);\n\t}\n\n\tpublic void put(String key, Object value) {\n\t\tif (value == null) {\n\t\t\tvalue = NULL;\n\t\t}\n\t\tprops.put(key, value);\n\t}\n\n\tpublic Object getObject(String key) {\n\t\tObject value = props.get(key);\n\t\tif (value == NULL) {\n\t\t\treturn null;\n\t\t}\n\t\treturn value;\n\t}\n\t\n\tpublic int size() {\n\t\treturn props.size();\n\t}\n\t\n\tpublic Iterator<String> iterator() {\n\t\treturn props.keySet().iterator();\n\t}\n\n\t@Override\n\tpublic String get(String key) {\n\t\tObject value = props.get(key);\n\t\treturn Objects.toString(value);\n\t}\n\t\n\tpublic Map<String, Object> getAll() {\n\t\treturn props;\n\t}\n}"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/value/ValueSource.java",
    "content": "package com.terran4j.commons.util.value;\n\npublic interface ValueSource<K, V> {\n\n    V get(K key);\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/value/ValueSources.java",
    "content": "package com.terran4j.commons.util.value;\n\nimport java.util.Stack;\n\npublic class ValueSources<K, V> implements ValueSource<K, V> {\n\t\n\tprivate Stack<ValueSource<K, V>> stack = new Stack<>();\n\n\t@Override\n\tpublic V get(K key) {\n\t\tif (key == null) {\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\tif (stack.size() == 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\tsynchronized (stack) {\n            for (int i = stack.size() - 1; i >= 0; i--) {\n                ValueSource<K, V> values = stack.get(i);\n                V value = values.get(key);\n                if (value != null) {\n                    return value;\n                }\n            }\n        }\n\n\t\treturn null;\n\t}\n\n\tpublic ValueSource<K, V> push(ValueSource<K, V> item) {\n\t\tif (item == null) {\n\t\t\tthrow new NullPointerException(\"push ValueGetter is null.\"); \n\t\t}\n\t\tstack.push(item);\n\t\treturn this;\n\t}\n\t\n\tpublic ValueSource<K, V> pop() {\n\t\treturn stack.pop();\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/value/ValueWrapper.java",
    "content": "package com.terran4j.commons.util.value;\n\npublic class ValueWrapper {\n\n\tprivate final ValueSource<String, String> values;\n\n\tpublic ValueWrapper(ValueSource<String, String> values) {\n\t\tsuper();\n\t\tthis.values = values;\n\t}\n\t\n\tpublic String get(String key) {\n\t\treturn values.get(key);\n\t}\n\t\n\tpublic int get(String key, int defaultValue) {\n\t\tString value = values.get(key);\n\t\tif (value == null) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\ttry {\n\t\t\treturn Integer.parseInt(value);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn defaultValue;\n\t\t}\n\t}\n\t\n\tpublic long get(String key, long defaultValue) {\n\t\tString value = values.get(key);\n\t\tif (value == null) {\n\t\t\treturn defaultValue;\n\t\t}\n\t\ttry {\n\t\t\treturn Long.parseLong(value);\n\t\t} catch (NumberFormatException e) {\n\t\t\treturn defaultValue;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/web/Cookies.java",
    "content": "package com.terran4j.commons.util.web;\n\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\n\nimport javax.servlet.http.Cookie;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\n\n/**\n * Cookie Util\n */\npublic class Cookies {\n\n    /**\n     * LOG\n     */\n\tprivate static final Logger log = LoggerFactory.getLogger(Cookies.class);\n\n    /**\n     * key value spliter\n     */\n    public static final String EQUALS = \"=\";\n\n    /**\n     * cookie secure\n     */\n    public static final String COOKIE_SECURE = \"Secure\";\n\n    /**\n     * set cookie http only\n     */\n    public static final String COOKIE_HTTP_ONLY = \"HttpOnly\";\n\n    /**\n     * set cookie identifier in response header\n     */\n    public static final String SET_COOKIE = \"Set-Cookie\";\n\n    /**\n     * Max-Age\n     */\n    public static final String MAX_AGE = \"Max-Age\";\n\n    /**\n     * COOKIE_SPLITER\n     */\n    public static final String COOKIE_SPLITER = \"; \";\n\n    /**\n     * setCookie\n     * \n     * @param response\n     *            response 请求的返回对象。\n     * @param cookieValues\n     *            cookieValues 要写入的 cookie 值。\n     */\n    public static void setCookie(HttpServletResponse response,\n            Map<String, String> cookieValues) {\n        setCookie(response, cookieValues, -1);\n    }\n\n    public static void setCookie(HttpServletResponse response, String key,\n            String value, int maxAge) {\n        if (StringUtils.isEmpty(key)) {\n            return;\n        }\n        Map<String, String> cookieValues = new HashMap<String, String>();\n        cookieValues.put(key, value);\n        setCookie(response, cookieValues, maxAge);\n    }\n\n    /**\n     * set cookie\n     * \n     * @param response\n     *            HttpServletResponse\n     * @param cookieValues\n     *            cookieValue map\n     * @param maxAge\n     *            cookie survive time -1:survived until closing explorer\n     *            0:remove this cookie int:the seconds of cookie surviving\n     */\n    public static void setCookie(HttpServletResponse response,\n            Map<String, String> cookieValues, int maxAge) {\n    \t\tif (cookieValues == null) {\n    \t\t\tthrow new NullPointerException(\"cookieValues is null.\");\n    \t\t}\n        Iterator<String> it = cookieValues.keySet().iterator();\n        while (it.hasNext()) {\n            String key = it.next();\n            if (key == null) {\n                continue;\n            }\n\n            String value = cookieValues.get(key);\n            if (value != null) {\n                Cookie cookieItem = new Cookie(key, value);\n                cookieItem.setPath(\"/\");\n                cookieItem.setMaxAge(maxAge);\n                response.addCookie(cookieItem);\n                if (log.isInfoEnabled()) {\n                    log.info(\"addCookie, key = \" + key + \", value = \" + value);\n                }\n            } else {\n                Cookie cookieItem = new Cookie(key, null);\n                cookieItem.setPath(\"/\");\n                cookieItem.setMaxAge(0);\n                response.addCookie(cookieItem);\n                if (log.isInfoEnabled()) {\n                    log.info(\"removeCookie, key = \" + key);\n                }\n            }\n        }\n    }\n\n    /**\n     * remove the cookie named cookieName\n     * \n     * @param response\n     *            HttpServletResponse\n     * @param cookieName\n     *            cookieName\n     */\n    public static void removeCookie(HttpServletResponse response,\n            String cookieName) {\n    \t\tif (StringUtils.isEmpty(cookieName)) {\n\t\t\tthrow new NullPointerException(\"cookieName is empty.\");\n\t\t}\n        Map<String, String> removeCookie = new HashMap<String, String>();\n        removeCookie.put(cookieName, null);\n        setCookie(response, removeCookie, 0);\n\n    }\n\n    /**\n     * get the cookie named cookie name\n     * \n     * @param request\n     *            HttpServletRequest\n     * @param cookieName\n     *            cookieName\n     * @return cookie value\n     */\n    public static String getCookie(HttpServletRequest request, String cookieName) {\n        if (request == null) {\n        \t\tthrow new NullPointerException(\"request is null.\");\n        }\n        if (StringUtils.isEmpty(cookieName)) {\n\t    \t\tthrow new NullPointerException(\"cookieName is empty.\");\n\t    }\n        Cookie[] cookies = request.getCookies();\n        if (cookies == null || cookies.length == 0) {\n            return null;\n        }\n        for (Cookie c : cookies) {\n            if (cookieName.equals(c.getName())) {\n                return c.getValue();\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "commons-util/src/main/java/com/terran4j/commons/util/web/IPAddresses.java",
    "content": "package com.terran4j.commons.util.web;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.InetAddress;\nimport java.net.NetworkInterface;\nimport java.net.SocketException;\nimport java.util.Enumeration;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\n\npublic class IPAddresses {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(IPAddresses.class);\n\n\t/**\n\t * 获取本地ip地址，有可能会有多个地址, 若有多个网卡则会搜集多个网卡的ip地址\n\t * @return 所有的网络地址。\n\t */\n\tpublic static Set<InetAddress> resolveLocalAddresses() {\n\t\tSet<InetAddress> addrs = new HashSet<InetAddress>();\n\t\tEnumeration<NetworkInterface> ns = null;\n\t\ttry {\n\t\t\tns = NetworkInterface.getNetworkInterfaces();\n\t\t} catch (SocketException e) {\n\t\t\t// ignored...\n\t\t}\n\t\twhile (ns != null && ns.hasMoreElements()) {\n\t\t\tNetworkInterface n = ns.nextElement();\n\t\t\tEnumeration<InetAddress> is = n.getInetAddresses();\n\t\t\twhile (is.hasMoreElements()) {\n\t\t\t\tInetAddress i = is.nextElement();\n\t\t\t\tif (!i.isLoopbackAddress() && !i.isLinkLocalAddress() && !i.isMulticastAddress()\n\t\t\t\t\t\t&& !isSpecialIp(i.getHostAddress()))\n\t\t\t\t\taddrs.add(i);\n\t\t\t}\n\t\t}\n\t\treturn addrs;\n\t}\n\n\tpublic static String resolveLocalIp() {\n\t\tSet<InetAddress> addrs = resolveLocalAddresses();\n\t\tfor (InetAddress addr : addrs) {\n\t\t\treturn addr.getHostAddress();\n\t\t}\n\t\treturn \"\";\n\t}\n\n\tpublic static Set<String> resolveLocalIps() {\n\t\tSet<InetAddress> addrs = resolveLocalAddresses();\n\t\tSet<String> ret = new HashSet<String>();\n\t\tfor (InetAddress addr : addrs)\n\t\t\tret.add(addr.getHostAddress());\n\t\treturn ret;\n\t}\n\n\tprivate static boolean isSpecialIp(String ip) {\n\t\tif (ip.contains(\":\"))\n\t\t\treturn true;\n\t\tif (ip.startsWith(\"127.\"))\n\t\t\treturn true;\n\t\tif (ip.startsWith(\"169.254.\"))\n\t\t\treturn true;\n\t\tif (ip.equals(\"255.255.255.255\"))\n\t\t\treturn true;\n\t\treturn false;\n\t}\n\n\tpublic static String getLocalHostName() {\n\t\tString hostname = System.getenv(\"HOSTNAME\");\n\t\tif (StringUtils.isEmpty(hostname)) {\n\t\t\ttry {\n\t\t\t\tInputStream in;\n\t\t\t\tProcess pro = Runtime.getRuntime().exec(\"hostname\");\n\t\t\t\tpro.waitFor();\n\t\t\t\tin = pro.getInputStream();\n\t\t\t\tBufferedReader read = new BufferedReader(new InputStreamReader(in));\n\t\t\t\thostname = read.readLine();\n\t\t\t} catch (IOException e) {\n\t\t\t\tlog.error(\"getLocalHostName IOException\");\n\t\t\t} catch (InterruptedException e) {\n\t\t\t\tlog.error(\"getLocalHostName InterruptedException\");\n\t\t\t}\n\t\t}\n\t\treturn hostname;\n\t}\n\t\n\t/**\n\t * 将ip转换为定长8个字符的16进制表示形式，如：255.255.255.255 成 FFFFFFFF\n\t * @param ip IP地址原始字符串\n\t * @return IP地址16进制字符串\n\t */\n\tpublic static String hex2IP(String ip) {\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfor (String seg : ip.split(\"\\\\.\")) {\n\t\t\tString h = Integer.toHexString(Integer.parseInt(seg));\n\t\t\tif (h.length() == 1)\n\t\t\t\tsb.append(\"0\");\n\t\t\tsb.append(h);\n\t\t}\n\t\treturn sb.toString();\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-util/src/main/java/error.properties",
    "content": "\ninternal.error = 服务内部错误\n\t\nduplicate.key = 关键字段不允许重复\n\t\nunknown.error = 未知错误\n\t\ninvalid.param = 无效的参数\n\t\nnull.param = 参数不能为空\n\nconfig.error = 配置错误\n\t\nresource.not.found = 请求资源不存在\n\t\nauth.failed = 登录认证失败\n\naccess.forbidden = 不允许访问此功能\n\t\naccess.deny = 访问被拒绝"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/ClassesTest.java",
    "content": "package com.terran4j.common.util;\n\nimport com.terran4j.commons.util.Classes;\nimport org.junit.Assert;\nimport org.junit.Test;\n\nimport java.lang.reflect.Method;\nimport java.util.LinkedList;\nimport java.util.List;\n\npublic class ClassesTest {\n\n    public Integer doAction(String name, List<Object> args) {\n        return null;\n    }\n\n    @Test\n    public void testToIdentify() throws Exception {\n        Object[] args = new Object[]{\"abc\", new LinkedList<>()};\n        Method method = Classes.getMethod(ClassesTest.class,\n                \"doAction\", args, null);\n        String methodId = Classes.toIdentify(method);\n        String expectId = \"com.terran4j.common.util.ClassesTest::doAction\" +\n                \"(java.lang.String,java.util.List)\";\n        Assert.assertEquals(expectId, methodId);\n    }\n\n    @Test\n    public void testParseWithResultType() throws Exception {\n        LinkedList<Object> list = new LinkedList<>();\n        Object[] args = new Object[]{\"abc\", list};\n        Method method = Classes.getMethod(ClassesTest.class,\n                \"doAction\", args, null);\n        Assert.assertNotNull(method);\n        Assert.assertEquals(Integer.class, method.getReturnType());\n    }\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/DateTimesTest.java",
    "content": "package com.terran4j.common.util;\n\nimport com.terran4j.commons.util.DateTimes;\nimport org.junit.Assert;\nimport org.junit.Test;\n\nimport java.util.Date;\n\npublic class DateTimesTest {\n\n    @Test\n    public void testCutHour() {\n        Date date = DateTimes.toDate(\"2017-03-02 18:41:59\");\n        Date cutDate = DateTimes.cutHour(date);\n        String dateText = DateTimes.toString(cutDate);\n        Assert.assertEquals(\"2017-03-02 00:00:00\", dateText);\n\n        date = DateTimes.toDate(\"2017-03-02 00:00:00\");\n        cutDate = DateTimes.cutHour(date);\n        dateText = DateTimes.toString(cutDate);\n        Assert.assertEquals(\"2017-03-02 00:00:00\", dateText);\n    }\n\n    @Test\n    public void testCutDay() {\n        Date date = DateTimes.toDate(\"2017-03-12 18:41:59\");\n        Date cutDate = DateTimes.cutDay(date);\n        String dateText = DateTimes.toString(cutDate);\n        Assert.assertEquals(\"2017-03-01 00:00:00\", dateText);\n\n        date = DateTimes.toDate(\"2017-03-01 00:00:00\");\n        cutDate = DateTimes.cutDay(date);\n        dateText = DateTimes.toString(cutDate);\n        Assert.assertEquals(\"2017-03-01 00:00:00\", dateText);\n    }\n\n    @Test\n    public void testCutMonth() {\n        Date date = DateTimes.toDate(\"2017-12-31 23:59:59\");\n        Date cutDate = DateTimes.cutMonth(date);\n        String dateText = DateTimes.toString(cutDate);\n        Assert.assertEquals(\"2017-01-01 00:00:00\", dateText);\n    }\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/EnumsTest.java",
    "content": "package com.terran4j.common.util;\n\nimport com.terran4j.commons.util.Enums;\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class EnumsTest {\n\n    public enum UserType {\n\n        root,\n\n        admin,\n\n        user,\n\n    }\n\n    @Test\n    public void testGetEnumObject() throws Throwable {\n        Object root = Enums.getEnumObject(UserType.class, \"root\");\n        Assert.assertEquals(UserType.root, root);\n        Object user = Enums.getEnumObject(UserType.class, \"user\");\n        Assert.assertEquals(UserType.user, user);\n    }\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/ExpressionsTest.java",
    "content": "package com.terran4j.common.util;\n\nimport com.terran4j.commons.util.Expressions;\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ExpressionsTest {\n\n    public static class User {\n\n        private String name;\n\n        private Date birthday;\n\n        public User() {\n        }\n\n        public User(String name, Date birthday) {\n            this.name = name;\n            this.birthday = birthday;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n        public Date getBirthday() {\n            return birthday;\n        }\n\n        public void setBirthday(Date birthday) {\n            this.birthday = birthday;\n        }\n    }\n\n    @Test\n    public void testParseWithResultType() throws Exception {\n        Map<String, Object> args = new HashMap<>();\n        User user = new User(\"neo\", new Date());\n        args.put(\"user\", user);\n\n        String name = Expressions.parse(\"#user.name\", args, String.class);\n        Assert.assertEquals(\"neo\", name);\n        Date birthday = Expressions.parse(\"#user.birthday\", args, Date.class);\n        Assert.assertEquals(user.getBirthday(), birthday);\n    }\n\n    @Test\n    public void testParseWithoutResultType() throws Exception {\n        Map<String, Object> args = new HashMap<>();\n        User user = new User(\"neo\", new Date());\n        args.put(\"user\", user);\n\n        Object name = Expressions.parse(\"#user.name\", args);\n        Assert.assertEquals(\"neo\", name);\n        Object birthday = Expressions.parse(\"#user.birthday\", args);\n        Assert.assertEquals(user.getBirthday(), birthday);\n    }\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/JsonConfigElementTest.java",
    "content": "package com.terran4j.common.util;\n\nimport com.terran4j.commons.util.Strings;\nimport com.terran4j.commons.util.config.ConfigElement;\nimport com.terran4j.commons.util.config.JsonConfigElement;\nimport lombok.Data;\nimport org.junit.Assert;\nimport org.junit.Test;\n\nimport java.util.List;\n\npublic class JsonConfigElementTest {\n\n    @Data\n    public static class Item {\n\n        private int id = 0;\n\n        private String name = \"\";\n\n        public Item() {\n        }\n\n        public Item(int id, String name) {\n            this.id = id;\n            this.name = name;\n        }\n    }\n\n    @Test\n    public void test() throws Exception {\n        String jsonText = Strings.getString(this.getClass(), \"JsonConfigElementTest.json\");\n        ConfigElement element = new JsonConfigElement(jsonText);\n        Assert.assertEquals(new Integer(33), element.attrAsInt(\"int\"));\n        Assert.assertEquals(\"my text\", element.attr(\"str\"));\n        Assert.assertEquals(false, element.attrAsBoolean(\"bool\"));\n\n        Item item1 = new Item(1, \"111\");\n        Assert.assertEquals(item1, element.attr(\"item1\", Item.class));\n        Item item2 = new Item(2, \"222\");\n        Assert.assertEquals(item2, element.attr(\"item2\", Item.class));\n\n        List<Item> items = element.getChildren(\"items\", Item.class);\n        Assert.assertNotNull(items);\n        Assert.assertEquals(item1, items.get(0));\n        Assert.assertEquals(item2, items.get(1));\n    }\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/JsonConfigElementTest.json",
    "content": "{\n  \"int\": 33,\n  \"str\": \"my text\",\n  \"bool\": false,\n  \"item1\": {\n    \"id\": 1,\n    \"name\": \"111\"\n  },\n  \"item2\": {\n    \"id\": 2,\n    \"name\": \"222\"\n  },\n  \"items\": [\n    {\n      \"id\": 1,\n      \"name\": \"111\"\n    },\n    {\n      \"id\": 2,\n      \"name\": \"222\"\n    }\n  ]\n}"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/JsonsTest.java",
    "content": "package com.terran4j.common.util;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport com.terran4j.commons.util.Jsons;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class JsonsTest {\n\n    private static final Logger log = LoggerFactory.getLogger(JsonsTest.class);\n\n    @Test\n    public void testJsonFormat() throws Exception {\n        log.info(\"testJsonFormat\");\n        String uglyJsonText = \"{\\\"data1\\\":100,\\\"data2\\\":\\\"hello\\\",\\\"list\\\":[\\\"String 1\\\",\\\"String 2\\\",\\\"String 3\\\"]}\";\n        String prettyJsonText = Jsons.format(uglyJsonText);\n        log.info(\"JSON格式化前：\\n{}\", uglyJsonText);\n        log.info(\"JSON格式化后：\\n{}\", prettyJsonText);\n        String exceptedJsonText = \"{\\n\" + \"  \\\"data1\\\": 100,\\n\" + \"  \\\"data2\\\": \\\"hello\\\",\\n\" + \"  \\\"list\\\": [\\n\"\n                + \"    \\\"String 1\\\",\\n\" + \"    \\\"String 2\\\",\\n\" + \"    \\\"String 3\\\"\\n\" + \"  ]\\n\" + \"}\";\n        Assert.assertEquals(exceptedJsonText, prettyJsonText.trim());\n    }\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/LoopExecuteTaskTest.java",
    "content": "package com.terran4j.common.util;\n\nimport java.util.concurrent.CountDownLatch;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport com.terran4j.commons.util.task.LoopExecuteTask;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class LoopExecuteTaskTest {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(LoopExecuteTaskTest.class);\n\n\t@Test\n\tpublic void testInterrupt() {\n\t\tlog.info(\"testInterrupt\");\n\t\tfinal CountDownLatch latch = new CountDownLatch(1);\n\t\tLoopExecuteTask task = new LoopExecuteTask(){\n\n\t\t\t@Override\n\t\t\tprotected boolean execute() throws Exception {\n\t\t\t\tif (latch.getCount() == 1) {\n\t\t\t\t\tlatch.countDown();\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t\n\t\t};\n\t\tThread thread = new Thread(task);\n\t\t\n\t\tthread.start();\n\t\ttry {\n\t\t\tlatch.await();\n\t\t} catch (InterruptedException e) {\n\t\t}\n\t\tAssert.assertTrue(task.isRunning());\n\t\t\n//\t\tthread.interrupt();\n//\t\ttry {\n//\t\t\tThread.sleep(500);\n//\t\t} catch (InterruptedException e) {\n//\t\t}\n//\t\tAssert.assertFalse(task.isRunning());\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/SecurityTest.java",
    "content": "package com.terran4j.common.util;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport com.terran4j.commons.util.security.AsymmetricKeys;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class SecurityTest {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(SecurityTest.class);\n\n\t@Test\n\tpublic void testEncryptAndDecrypt() throws Exception {\n\t\tAsymmetricKeys rsa = new AsymmetricKeys();\n\t\tString plainText = \"Hello, world!\"; // 测试字符串\n\t\tlog.info(\"plainText: {}\", plainText);\n\t\tString cipherText = rsa.encrypt(plainText); // 加密\n\t\tlog.info(\"cipherText: {}\", cipherText);\n\t\tString resultText = rsa.decrypt(cipherText); // 解密\n\t\tlog.info(\"resultText: {}\", resultText);\n\t\tAssert.assertEquals(plainText, resultText);\n\t}\n\t\n\t@Test\n\tpublic void testCreateByText() throws Exception {\n\t\tAsymmetricKeys rsa0 = new AsymmetricKeys();\n\t\tString publicKey = rsa0.getPublicKey();\n\t\tlog.info(\"publicKey: \\n{}\", publicKey);\n\t\tString privateKey = rsa0.getPrivateKey();\n\t\tlog.info(\"privateKey: \\n{}\", privateKey);\n\t\tAsymmetricKeys rsa1 = new AsymmetricKeys(publicKey, privateKey);\n\t\tString plainText = \"Hello, world!\";\n\t\tString cipherText = rsa0.encrypt(plainText); // rsa0 加密\n\t\tString resultText = rsa1.decrypt(cipherText); // rsa1 解密\n\t\tAssert.assertEquals(plainText, resultText);\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/StringsTest.java",
    "content": "package com.terran4j.common.util;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport com.terran4j.commons.util.Strings;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class StringsTest {\n\t\n\tprivate static final Logger log = LoggerFactory.getLogger(StringsTest.class);\n\n\t@Test\n\tpublic void testHex2String() throws Exception {\n\t\tString sourceText = \"Hello, world!\";\n\t\tbyte[] sourceData = sourceText.getBytes();\n\t\tlog.info(\"sourceData = {}\", sourceData);\n\t\tString hexText = Strings.toHexString(sourceData);\n\t\tlog.info(\"hexText = {}\", hexText);\n\t\tbyte[] resolvedData = Strings.fromHexString(hexText);\n\t\tlog.info(\"resolvedData = {}\", resolvedData);\n\t\tString resultText = new String(resolvedData);\n\t\tlog.info(\"resultText = {}\", resultText);\n\t\tAssert.assertEquals(sourceText, resultText);\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/ValueTest.java",
    "content": "package com.terran4j.common.util;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.util.StringUtils;\n\nimport com.terran4j.commons.util.value.MapValueSource;\nimport com.terran4j.commons.util.value.ValueWrapper;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class ValueTest {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(ValueTest.class);\n\t\n\tprivate Map<String, String> getConfigs() {\n\t\tMap<String, String> config = new HashMap<>();\n\t\tconfig.put(\"count\", \"123\");\n\t\treturn config;\n\t}\n\n\tpublic void demo() {\n\t\t// 将配置文件数据读到一个 map 对象中。\n\t\tMap<String, String> configs = getConfigs();\n\n\t\t// 取键为\"count\"的值，并按int值处理，如果没有或格式不是数字，则取默认值为 0.\n\t\tint valueAsInt = 0;\n\t\tString key = \"count\";\n\t\tString value = configs.get(key);\n\t\tif (!StringUtils.isEmpty(value)) {\n\t\t\ttry {\n\t\t\t\tvalueAsInt = Integer.parseInt(value.trim());\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tlog.error(\"key[{}] expect int format, but the value is: {}\", key, value);\n\t\t\t}\n\t\t} else {\n\t\t\tlog.error(\"key[{}]'s value is null\", key);\n\t\t}\n\t\tAssert.assertEquals(123, valueAsInt);\n\t}\n\t\n\t@Test\n\tpublic void testGetValue() {\n\t\t// 将配置文件数据读到一个 map 对象中。\n\t\tMap<String, String> configs = getConfigs();\n\t\t\n\t\t// 将 map 包装成 ValueSource 形式，放到 ValueWrapper 中，再用 ValueWrapper 工具类来取值。\n\t\tValueWrapper values = new ValueWrapper(new MapValueSource<String, String>(configs));\n\t\tAssert.assertEquals(123, values.get(\"count\", 0));\n\t}\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/error/BusinessExceptionTest.java",
    "content": "package com.terran4j.common.util.error;\n\nimport org.junit.Assert;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\n\nimport com.terran4j.commons.util.error.BusinessException;\nimport com.terran4j.commons.util.error.ErrorCodes;\nimport com.terran4j.commons.util.error.ErrorReport;\n\n@RunWith(SpringJUnit4ClassRunner.class)\npublic class BusinessExceptionTest {\n\n\tprivate static final Logger log = LoggerFactory.getLogger(BusinessExceptionTest.class);\n\n\t@Test\n\tpublic void testGetReport() {\n\t\ttry {\n\t\t\tString mobile = \"13800000000\";\n\t\t\tString comment = \"ABC\";\n\t\t\tcommentByMobile(mobile, comment);\n\t\t} catch (BusinessException e) {\n\t\t\tErrorReport report = e.getReport();\n\t\t\tSystem.out.println(report.toString());\n\t\t\tAssert.assertEquals(MockErrorCode.INVALID_CONFIG_VALUE.getValue(), report.getCode().getValue());\n\t\t\treturn;\n\t\t}\n\t}\n\n\t@Test\n\tpublic void testDefaultErrorCodes() {\n\t\ttry {\n\t\t\tthrow new BusinessException(ErrorCodes.ACCESS_DENY);\n\t\t} catch (BusinessException e) {\n\t\t\tString msg = e.getMessage();\n\t\t\tlog.info(ErrorCodes.ACCESS_DENY + \" = \" + msg);\n\t\t\tAssert.assertEquals(\"访问被拒绝\", msg);\n\t\t}\n\t}\n\n\tpublic String getConfig(String key) {\n\t\treturn \"abc\";\n\t}\n\n\tpublic int getConfigAsInt(String key) throws BusinessException {\n\t\tString value = getConfig(key);\n\t\ttry {\n\t\t\treturn Integer.parseInt(value);\n\t\t} catch (NumberFormatException e) {\n\t\t\tthrow new BusinessException(MockErrorCode.INVALID_CONFIG_VALUE, e) //\n\t\t\t\t\t.put(\"key\", key).put(\"value\", value) //\n\t\t\t\t\t.setMessage(\"获取配置项[${key}]时出错，值[${value}]不是一个数字\");\n\t\t}\n\t}\n\n\tpublic long getPoint(long userId, String event) throws BusinessException {\n\t\ttry {\n\t\t\tString key = \"point.\" + event;\n\t\t\treturn getConfigAsInt(key);\n\t\t} catch (BusinessException e) {\n\t\t\tthrow e.reThrow(\"获取用户[${userId}]在事件[${event}]中对应的积分值时出错\") //\n\t\t\t\t\t.put(\"event\", event).put(\"userId\", userId);\n\t\t}\n\t}\n\n\tpublic void plusPoint(long userId, long point) {\n\t}\n\n\tpublic void plusPoint(long userId, String event) throws BusinessException {\n\t\ttry {\n\t\t\tlong point = getPoint(userId, event);\n\t\t\tplusPoint(userId, point);\n\t\t} catch (BusinessException e) {\n\t\t\tthrow e.reThrow(\"用户[${userId}]参与事件[${event}]后，在增加积分时出错。\") //\n\t\t\t\t\t.put(\"event\", event).put(\"userId\", userId);\n\t\t}\n\t}\n\n\tpublic long getUserId(String mobile) {\n\t\treturn 1;\n\t}\n\n\tpublic void commentByMobile(String mobile, String comment) throws BusinessException {\n\t\ttry {\n\t\t\tlong userId = getUserId(mobile);\n\t\t\tplusPoint(userId, \"commentByMobile\");\n\t\t} catch (BusinessException e) {\n\t\t\tthrow e.reThrow(\"用户[${mobile}]在参加手机评论活动时出错\") //\n\t\t\t\t\t.put(\"mobile\", mobile);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/error/MockErrorCode.java",
    "content": "package com.terran4j.common.util.error;\n\nimport com.terran4j.commons.util.error.ErrorCode;\n\npublic enum MockErrorCode implements ErrorCode {\n\t\n\tINVALID_CONFIG_VALUE(101, \"invalid.config.value\"),\n\t\n\t;\n\n\tprivate final int value;\n\t\n\tprivate final String name;\n\n\tprivate MockErrorCode(int value, String name) {\n\t\tthis.value = value;\n\t\tthis.name = name;\n\t}\n\n\tpublic final int getValue() {\n\t\treturn value;\n\t}\n\t\n\tpublic final String getName() {\n\t\treturn name;\n\t}\n\t\n}\n"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/error/MockException.properties",
    "content": "\nmock.error.1 = Mock Error 1\nmock.error.2 = Mock Error 2"
  },
  {
    "path": "commons-util/src/test/java/com/terran4j/common/util/error/testGetReport.txt",
    "content": "com.terran4j.common.util.error.BaseExceptionTest.testGetReport(BaseExceptionTest.java:37)\ncode = 102, mock.error.2\nMore Detail Information:\n    b: bbb\ncause by:  com.terran4j.common.util.error.BaseExceptionTest.testGetReport(BaseExceptionTest.java:33)\ncode = 101, mock.error.1\nMore Detail Information:\n    a: aaa\ncause by:  com.terran4j.common.util.error.BaseExceptionTest.testGetReport(BaseExceptionTest.java:31)\njava.lang.RuntimeException: 000\n\tat com.terran4j.common.util.error.BaseExceptionTest.testGetReport(BaseExceptionTest.java:31)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)\n\tat org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)\n\tat org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)\n\tat org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)\n\tat org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)\n\tat org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)\n\tat org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)\n\tat org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)\n\tat org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)\n\tat org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)\n\tat org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)\n\tat org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)\n\tat org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)\n\tat org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)\n\tat org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)\n\tat org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)\n\tat org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)\n\tat org.junit.runners.ParentRunner.run(ParentRunner.java:363)\n\tat org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)\n\tat org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)\n\tat org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)\n\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)\n\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)\n\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)\n\tat org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)\n"
  },
  {
    "path": "commons-util/src/test/java/error.properties",
    "content": "\t\naccess.deny = 访问被拒绝2"
  },
  {
    "path": "commons-website/.gitignore",
    "content": "/target/\n/.settings/\n/.classpath\n/.project\n*.iml\n"
  },
  {
    "path": "commons-website/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.github.terran4j</groupId>\n        <artifactId>terran4j-commons-parent</artifactId>\n        <version>1.0.4-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>terran4j-commons-website</artifactId>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <!-- 模板引擎 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-freemarker</artifactId>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "commons-website/src/main/java/com/terran4j/commons/website/config/Readme.java",
    "content": "package com.terran4j.commons.website.config;\n\n/**\n *  这是一个用于网站项目的工具类库。\n *\n *  @author terran4j\n */\npublic class Readme {\n\n    /**\n     * 什么也不做。\n     */\n    public void doNothing() {\n    }\n\n}\n"
  },
  {
    "path": "commons-website/src/main/java/com/terran4j/commons/website/config/WebsiteConfiguration.java",
    "content": "package com.terran4j.commons.website.config;\n\nimport com.terran4j.commons.website.controller.WelcomeController;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;\n\n@Configuration\npublic class WebsiteConfiguration extends WebMvcConfigurerAdapter {\n\n    private static final String PATH_PATTERN = \"/**\";\n\n    private static final String PATH_LOCATION = \"classpath:/static/\";\n\n    @Bean\n    public WelcomeController welcomeController() {\n        return new WelcomeController();\n    }\n\n    public void addResourceHandlers(ResourceHandlerRegistry registry) {\n        /**\n         * 一旦启用了 @EnableWebMvc，则 Spring Boot 中内置的 MVC 规则就不好使了。\n         * 因此这里手工添加静态资源映射关系。\n         */\n        registry.addResourceHandler(PATH_PATTERN).addResourceLocations(PATH_LOCATION);\n        super.addResourceHandlers(registry);\n    }\n}\n"
  },
  {
    "path": "commons-website/src/main/java/com/terran4j/commons/website/controller/WelcomeController.java",
    "content": "package com.terran4j.commons.website.controller;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.util.StringUtils;\nimport org.springframework.web.bind.annotation.RequestMapping;\n\n@Controller\npublic class WelcomeController {\n\n    @Value(\"${server.website.welcome:index.html}\")\n    private String welcomePath = \"index.html\";\n\n    /**\n     * 欢迎页\n     */\n    @RequestMapping(\"/\")\n    public String index() {\n        if (StringUtils.isEmpty(welcomePath)) {\n            welcomePath = \"index.html\";\n        }\n        String path = welcomePath.trim();\n        if (\"/\" == path) {\n            return null;\n        }\n        if (!path.startsWith(\"/\")) {\n            path = \"/\" + path;\n        }\n        return \"redirect:\" + path;\n    }\n\n}\n"
  },
  {
    "path": "commons-website/src/main/resources/static/website/flexible-lite/flexible-lite-1.0.js",
    "content": "/**\n * @param designWidth:  设计稿的实际宽度值，需要根据实际设置\n */\nfunction flex(designWidth) {\n    var doc = document,\n        win = window,\n        docEl = doc.documentElement,\n        remStyle = document.createElement(\"style\"),\n        tid; // setTimeout 的句柄。\n\n    function refreshRem() {\n        var width = docEl.getBoundingClientRect().width;\n        var rem = width * 10 / designWidth;\n        remStyle.innerHTML = 'html{font-size:' + rem + 'px;}';\n    }\n\n    if (docEl.firstElementChild) {\n        docEl.firstElementChild.appendChild(remStyle);\n    } else {\n        var wrap = doc.createElement(\"div\");\n        wrap.appendChild(remStyle);\n        doc.write(wrap.innerHTML);\n        wrap = null;\n    }\n    //要等 viewport 设置好后才能执行 refreshRem，不然 refreshRem 会执行2次；\n    refreshRem();\n\n    win.addEventListener(\"resize\", function () {\n        clearTimeout(tid); //防止执行两次\n        tid = setTimeout(refreshRem, 300);\n    }, false);\n\n    win.addEventListener(\"pageshow\", function (e) {\n        if (e.persisted) { // 浏览器后退的时候重新计算\n            clearTimeout(tid);\n            tid = setTimeout(refreshRem, 300);\n        }\n    }, false);\n\n    if (doc.readyState === \"complete\") {\n        doc.body.style.fontSize = \"16px\";\n    } else {\n        doc.addEventListener(\"DOMContentLoaded\", function (e) {\n            doc.body.style.fontSize = \"16px\";\n        }, false);\n    }\n}"
  },
  {
    "path": "commons-website/src/main/resources/static/website/less/less-1.7.0.js",
    "content": "/*! \n * LESS - Leaner CSS v1.7.0 \n * http://lesscss.org \n * \n * Copyright (c) 2009-2014, Alexis Sellier <self@cloudhead.net> \n * Licensed under the Apache v2 License. \n * \n */ \n\n /** * @license Apache v2\n */ \n\n\n\n(function (window, undefined) {//\n// Stub out `require` in the browser\n//\nfunction require(arg) {\n    return window.less[arg.split('/')[1]];\n};\n\n\nif (typeof(window.less) === 'undefined' || typeof(window.less.nodeType) !== 'undefined') { window.less = {}; }\nless = window.less;\ntree = window.less.tree = {};\nless.mode = 'browser';\n\nvar less, tree;\n\n// Node.js does not have a header file added which defines less\nif (less === undefined) {\n    less = exports;\n    tree = require('./tree');\n    less.mode = 'node';\n}\n//\n// less.js - parser\n//\n//    A relatively straight-forward predictive parser.\n//    There is no tokenization/lexing stage, the input is parsed\n//    in one sweep.\n//\n//    To make the parser fast enough to run in the browser, several\n//    optimization had to be made:\n//\n//    - Matching and slicing on a huge input is often cause of slowdowns.\n//      The solution is to chunkify the input into smaller strings.\n//      The chunks are stored in the `chunks` var,\n//      `j` holds the current chunk index, and `currentPos` holds\n//      the index of the current chunk in relation to `input`.\n//      This gives us an almost 4x speed-up.\n//\n//    - In many cases, we don't need to match individual tokens;\n//      for example, if a value doesn't hold any variables, operations\n//      or dynamic references, the parser can effectively 'skip' it,\n//      treating it as a literal.\n//      An example would be '1px solid #000' - which evaluates to itself,\n//      we don't need to know what the individual components are.\n//      The drawback, of course is that you don't get the benefits of\n//      syntax-checking on the CSS. This gives us a 50% speed-up in the parser,\n//      and a smaller speed-up in the code-gen.\n//\n//\n//    Token matching is done with the `$` function, which either takes\n//    a terminal string or regexp, or a non-terminal function to call.\n//    It also takes care of moving all the indices forwards.\n//\n//\nless.Parser = function Parser(env) {\n    var input,       // LeSS input string\n        i,           // current index in `input`\n        j,           // current chunk\n        saveStack = [],   // holds state for backtracking\n        furthest,    // furthest index the parser has gone to\n        chunks,      // chunkified input\n        current,     // current chunk\n        currentPos,  // index of current chunk, in `input`\n        parser,\n        parsers,\n        rootFilename = env && env.filename;\n\n    // Top parser on an import tree must be sure there is one \"env\"\n    // which will then be passed around by reference.\n    if (!(env instanceof tree.parseEnv)) {\n        env = new tree.parseEnv(env);\n    }\n\n    var imports = this.imports = {\n        paths: env.paths || [],  // Search paths, when importing\n        queue: [],               // Files which haven't been imported yet\n        files: env.files,        // Holds the imported parse trees\n        contents: env.contents,  // Holds the imported file contents\n        contentsIgnoredChars: env.contentsIgnoredChars, // lines inserted, not in the original less\n        mime:  env.mime,         // MIME type of .less files\n        error: null,             // Error in parsing/evaluating an import\n        push: function (path, currentFileInfo, importOptions, callback) {\n            var parserImports = this;\n            this.queue.push(path);\n\n            var fileParsedFunc = function (e, root, fullPath) {\n                parserImports.queue.splice(parserImports.queue.indexOf(path), 1); // Remove the path from the queue\n\n                var importedPreviously = fullPath === rootFilename;\n\n                parserImports.files[fullPath] = root;                        // Store the root\n\n                if (e && !parserImports.error) { parserImports.error = e; }\n\n                callback(e, root, importedPreviously, fullPath);\n            };\n\n            if (less.Parser.importer) {\n                less.Parser.importer(path, currentFileInfo, fileParsedFunc, env);\n            } else {\n                less.Parser.fileLoader(path, currentFileInfo, function(e, contents, fullPath, newFileInfo) {\n                    if (e) {fileParsedFunc(e); return;}\n\n                    var newEnv = new tree.parseEnv(env);\n\n                    newEnv.currentFileInfo = newFileInfo;\n                    newEnv.processImports = false;\n                    newEnv.contents[fullPath] = contents;\n\n                    if (currentFileInfo.reference || importOptions.reference) {\n                        newFileInfo.reference = true;\n                    }\n\n                    if (importOptions.inline) {\n                        fileParsedFunc(null, contents, fullPath);\n                    } else {\n                        new(less.Parser)(newEnv).parse(contents, function (e, root) {\n                            fileParsedFunc(e, root, fullPath);\n                        });\n                    }\n                }, env);\n            }\n        }\n    };\n\n    function save()    { currentPos = i; saveStack.push( { current: current, i: i, j: j }); }\n    function restore() { var state = saveStack.pop(); current = state.current; currentPos = i = state.i; j = state.j; }\n    function forget() { saveStack.pop(); }\n\n    function sync() {\n        if (i > currentPos) {\n            current = current.slice(i - currentPos);\n            currentPos = i;\n        }\n    }\n    function isWhitespace(str, pos) {\n        var code = str.charCodeAt(pos | 0);\n        return (code <= 32) && (code === 32 || code === 10 || code === 9);\n    }\n    //\n    // Parse from a token, regexp or string, and move forward if match\n    //\n    function $(tok) {\n        var tokType = typeof tok,\n            match, length;\n\n        // Either match a single character in the input,\n        // or match a regexp in the current chunk (`current`).\n        //\n        if (tokType === \"string\") {\n            if (input.charAt(i) !== tok) {\n                return null;\n            }\n            skipWhitespace(1);\n            return tok;\n        }\n\n        // regexp\n        sync ();\n        if (! (match = tok.exec(current))) {\n            return null;\n        }\n\n        length = match[0].length;\n\n        // The match is confirmed, add the match length to `i`,\n        // and consume any extra white-space characters (' ' || '\\n')\n        // which come after that. The reason for this is that LeSS's\n        // grammar is mostly white-space insensitive.\n        //\n        skipWhitespace(length);\n\n        if(typeof(match) === 'string') {\n            return match;\n        } else {\n            return match.length === 1 ? match[0] : match;\n        }\n    }\n\n    // Specialization of $(tok)\n    function $re(tok) {\n        if (i > currentPos) {\n            current = current.slice(i - currentPos);\n            currentPos = i;\n        }\n        var m = tok.exec(current);\n        if (!m) {\n            return null;\n        }\n\n        skipWhitespace(m[0].length);\n        if(typeof m === \"string\") {\n            return m;\n        }\n\n        return m.length === 1 ? m[0] : m;\n    }\n\n    var _$re = $re;\n\n    // Specialization of $(tok)\n    function $char(tok) {\n        if (input.charAt(i) !== tok) {\n            return null;\n        }\n        skipWhitespace(1);\n        return tok;\n    }\n\n    function skipWhitespace(length) {\n        var oldi = i, oldj = j,\n            curr = i - currentPos,\n            endIndex = i + current.length - curr,\n            mem = (i += length),\n            inp = input,\n            c;\n\n        for (; i < endIndex; i++) {\n            c = inp.charCodeAt(i);\n            if (c > 32) {\n                break;\n            }\n\n            if ((c !== 32) && (c !== 10) && (c !== 9) && (c !== 13)) {\n                break;\n            }\n         }\n\n        current = current.slice(length + i - mem + curr);\n        currentPos = i;\n\n        if (!current.length && (j < chunks.length - 1)) {\n            current = chunks[++j];\n            skipWhitespace(0); // skip space at the beginning of a chunk\n            return true; // things changed\n        }\n\n        return oldi !== i || oldj !== j;\n    }\n\n    function expect(arg, msg) {\n        // some older browsers return typeof 'function' for RegExp\n        var result = (Object.prototype.toString.call(arg) === '[object Function]') ? arg.call(parsers) : $(arg);\n        if (result) {\n            return result;\n        }\n        error(msg || (typeof(arg) === 'string' ? \"expected '\" + arg + \"' got '\" + input.charAt(i) + \"'\"\n                                               : \"unexpected token\"));\n    }\n\n    // Specialization of expect()\n    function expectChar(arg, msg) {\n        if (input.charAt(i) === arg) {\n            skipWhitespace(1);\n            return arg;\n        }\n        error(msg || \"expected '\" + arg + \"' got '\" + input.charAt(i) + \"'\");\n    }\n\n    function error(msg, type) {\n        var e = new Error(msg);\n        e.index = i;\n        e.type = type || 'Syntax';\n        throw e;\n    }\n\n    // Same as $(), but don't change the state of the parser,\n    // just return the match.\n    function peek(tok) {\n        if (typeof(tok) === 'string') {\n            return input.charAt(i) === tok;\n        } else {\n            return tok.test(current);\n        }\n    }\n\n    // Specialization of peek()\n    function peekChar(tok) {\n        return input.charAt(i) === tok;\n    }\n\n\n    function getInput(e, env) {\n        if (e.filename && env.currentFileInfo.filename && (e.filename !== env.currentFileInfo.filename)) {\n            return parser.imports.contents[e.filename];\n        } else {\n            return input;\n        }\n    }\n\n    function getLocation(index, inputStream) {\n        var n = index + 1,\n            line = null,\n            column = -1;\n\n        while (--n >= 0 && inputStream.charAt(n) !== '\\n') {\n            column++;\n        }\n\n        if (typeof index === 'number') {\n            line = (inputStream.slice(0, index).match(/\\n/g) || \"\").length;\n        }\n\n        return {\n            line: line,\n            column: column\n        };\n    }\n\n    function getDebugInfo(index, inputStream, env) {\n        var filename = env.currentFileInfo.filename;\n        if(less.mode !== 'browser' && less.mode !== 'rhino') {\n            filename = require('path').resolve(filename);\n        }\n\n        return {\n            lineNumber: getLocation(index, inputStream).line + 1,\n            fileName: filename\n        };\n    }\n\n    function LessError(e, env) {\n        var input = getInput(e, env),\n            loc = getLocation(e.index, input),\n            line = loc.line,\n            col  = loc.column,\n            callLine = e.call && getLocation(e.call, input).line,\n            lines = input.split('\\n');\n\n        this.type = e.type || 'Syntax';\n        this.message = e.message;\n        this.filename = e.filename || env.currentFileInfo.filename;\n        this.index = e.index;\n        this.line = typeof(line) === 'number' ? line + 1 : null;\n        this.callLine = callLine + 1;\n        this.callExtract = lines[callLine];\n        this.stack = e.stack;\n        this.column = col;\n        this.extract = [\n            lines[line - 1],\n            lines[line],\n            lines[line + 1]\n        ];\n    }\n\n    LessError.prototype = new Error();\n    LessError.prototype.constructor = LessError;\n\n    this.env = env = env || {};\n\n    // The optimization level dictates the thoroughness of the parser,\n    // the lower the number, the less nodes it will create in the tree.\n    // This could matter for debugging, or if you want to access\n    // the individual nodes in the tree.\n    this.optimization = ('optimization' in this.env) ? this.env.optimization : 1;\n\n    //\n    // The Parser\n    //\n    parser = {\n\n        imports: imports,\n        //\n        // Parse an input string into an abstract syntax tree,\n        // @param str A string containing 'less' markup\n        // @param callback call `callback` when done.\n        // @param [additionalData] An optional map which can contains vars - a map (key, value) of variables to apply\n        //\n        parse: function (str, callback, additionalData) {\n            var root, line, lines, error = null, globalVars, modifyVars, preText = \"\";\n\n            i = j = currentPos = furthest = 0;\n\n            globalVars = (additionalData && additionalData.globalVars) ? less.Parser.serializeVars(additionalData.globalVars) + '\\n' : '';\n            modifyVars = (additionalData && additionalData.modifyVars) ? '\\n' + less.Parser.serializeVars(additionalData.modifyVars) : '';\n\n            if (globalVars || (additionalData && additionalData.banner)) {\n                preText = ((additionalData && additionalData.banner) ? additionalData.banner : \"\") + globalVars;\n                parser.imports.contentsIgnoredChars[env.currentFileInfo.filename] = preText.length;\n            }\n\n            str = str.replace(/\\r\\n/g, '\\n');\n            // Remove potential UTF Byte Order Mark\n            input = str = preText + str.replace(/^\\uFEFF/, '') + modifyVars;\n            parser.imports.contents[env.currentFileInfo.filename] = str;\n\n            // Split the input into chunks.\n            chunks = (function (input) {\n                var len = input.length, level = 0, parenLevel = 0,\n                    lastOpening, lastOpeningParen, lastMultiComment, lastMultiCommentEndBrace,\n                    chunks = [], emitFrom = 0,\n                    parserCurrentIndex, currentChunkStartIndex, cc, cc2, matched;\n\n                function fail(msg, index) {\n                    error = new(LessError)({\n                        index: index || parserCurrentIndex,\n                        type: 'Parse',\n                        message: msg,\n                        filename: env.currentFileInfo.filename\n                    }, env);\n                }\n\n                function emitChunk(force) {\n                    var len = parserCurrentIndex - emitFrom;\n                    if (((len < 512) && !force) || !len) {\n                        return;\n                    }\n                    chunks.push(input.slice(emitFrom, parserCurrentIndex + 1));\n                    emitFrom = parserCurrentIndex + 1;\n                }\n\n                for (parserCurrentIndex = 0; parserCurrentIndex < len; parserCurrentIndex++) {\n                    cc = input.charCodeAt(parserCurrentIndex);\n                    if (((cc >= 97) && (cc <= 122)) || (cc < 34)) {\n                        // a-z or whitespace\n                        continue;\n                    }\n\n                    switch (cc) {\n                        case 40:                        // (\n                            parenLevel++; \n                            lastOpeningParen = parserCurrentIndex; \n                            continue;\n                        case 41:                        // )\n                            if (--parenLevel < 0) {\n                                return fail(\"missing opening `(`\");\n                            }\n                            continue;\n                        case 59:                        // ;\n                            if (!parenLevel) { emitChunk(); }\n                            continue;\n                        case 123:                       // {\n                            level++; \n                            lastOpening = parserCurrentIndex; \n                            continue;\n                        case 125:                       // }\n                            if (--level < 0) {\n                                return fail(\"missing opening `{`\");\n                            }\n                            if (!level && !parenLevel) { emitChunk(); }\n                            continue;\n                        case 92:                        // \\\n                            if (parserCurrentIndex < len - 1) { parserCurrentIndex++; continue; }\n                            return fail(\"unescaped `\\\\`\");\n                        case 34:\n                        case 39:\n                        case 96:                        // \", ' and `\n                            matched = 0;\n                            currentChunkStartIndex = parserCurrentIndex;\n                            for (parserCurrentIndex = parserCurrentIndex + 1; parserCurrentIndex < len; parserCurrentIndex++) {\n                                cc2 = input.charCodeAt(parserCurrentIndex);\n                                if (cc2 > 96) { continue; }\n                                if (cc2 == cc) { matched = 1; break; }\n                                if (cc2 == 92) {        // \\\n                                    if (parserCurrentIndex == len - 1) {\n                                        return fail(\"unescaped `\\\\`\");\n                                    }\n                                    parserCurrentIndex++;\n                                }\n                            }\n                            if (matched) { continue; }\n                            return fail(\"unmatched `\" + String.fromCharCode(cc) + \"`\", currentChunkStartIndex);\n                        case 47:                        // /, check for comment\n                            if (parenLevel || (parserCurrentIndex == len - 1)) { continue; }\n                            cc2 = input.charCodeAt(parserCurrentIndex + 1);\n                            if (cc2 == 47) {\n                                // //, find lnfeed\n                                for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len; parserCurrentIndex++) {\n                                    cc2 = input.charCodeAt(parserCurrentIndex);\n                                    if ((cc2 <= 13) && ((cc2 == 10) || (cc2 == 13))) { break; }\n                                }\n                            } else if (cc2 == 42) {\n                                // /*, find */\n                                lastMultiComment = currentChunkStartIndex = parserCurrentIndex;\n                                for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len - 1; parserCurrentIndex++) {\n                                    cc2 = input.charCodeAt(parserCurrentIndex);\n                                    if (cc2 == 125) { lastMultiCommentEndBrace = parserCurrentIndex; }\n                                    if (cc2 != 42) { continue; }\n                                    if (input.charCodeAt(parserCurrentIndex + 1) == 47) { break; }\n                                }\n                                if (parserCurrentIndex == len - 1) {\n                                    return fail(\"missing closing `*/`\", currentChunkStartIndex);\n                                }\n                                parserCurrentIndex++;\n                            }\n                            continue;\n                        case 42:                       // *, check for unmatched */\n                            if ((parserCurrentIndex < len - 1) && (input.charCodeAt(parserCurrentIndex + 1) == 47)) {\n                                return fail(\"unmatched `/*`\");\n                            }\n                            continue;\n                    }\n                }\n\n                if (level !== 0) {\n                    if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) {\n                        return fail(\"missing closing `}` or `*/`\", lastOpening);\n                    } else {\n                        return fail(\"missing closing `}`\", lastOpening);\n                    }\n                } else if (parenLevel !== 0) {\n                    return fail(\"missing closing `)`\", lastOpeningParen);\n                }\n\n                emitChunk(true);\n                return chunks;\n            })(str);\n\n            if (error) {\n                return callback(new(LessError)(error, env));\n            }\n\n            current = chunks[0];\n\n            // Start with the primary rule.\n            // The whole syntax tree is held under a Ruleset node,\n            // with the `root` property set to true, so no `{}` are\n            // output. The callback is called when the input is parsed.\n            try {\n                root = new(tree.Ruleset)(null, this.parsers.primary());\n                root.root = true;\n                root.firstRoot = true;\n            } catch (e) {\n                return callback(new(LessError)(e, env));\n            }\n\n            root.toCSS = (function (evaluate) {\n                return function (options, variables) {\n                    options = options || {};\n                    var evaldRoot,\n                        css,\n                        evalEnv = new tree.evalEnv(options);\n                        \n                    //\n                    // Allows setting variables with a hash, so:\n                    //\n                    //   `{ color: new(tree.Color)('#f01') }` will become:\n                    //\n                    //   new(tree.Rule)('@color',\n                    //     new(tree.Value)([\n                    //       new(tree.Expression)([\n                    //         new(tree.Color)('#f01')\n                    //       ])\n                    //     ])\n                    //   )\n                    //\n                    if (typeof(variables) === 'object' && !Array.isArray(variables)) {\n                        variables = Object.keys(variables).map(function (k) {\n                            var value = variables[k];\n\n                            if (! (value instanceof tree.Value)) {\n                                if (! (value instanceof tree.Expression)) {\n                                    value = new(tree.Expression)([value]);\n                                }\n                                value = new(tree.Value)([value]);\n                            }\n                            return new(tree.Rule)('@' + k, value, false, null, 0);\n                        });\n                        evalEnv.frames = [new(tree.Ruleset)(null, variables)];\n                    }\n\n                    try {\n                        var preEvalVisitors = [],\n                            visitors = [\n                                new(tree.joinSelectorVisitor)(),\n                                new(tree.processExtendsVisitor)(),\n                                new(tree.toCSSVisitor)({compress: Boolean(options.compress)})\n                            ], i, root = this;\n\n                        if (options.plugins) {\n                            for(i =0; i < options.plugins.length; i++) {\n                                if (options.plugins[i].isPreEvalVisitor) {\n                                    preEvalVisitors.push(options.plugins[i]);\n                                } else {\n                                    if (options.plugins[i].isPreVisitor) {\n                                        visitors.splice(0, 0, options.plugins[i]);\n                                    } else {\n                                        visitors.push(options.plugins[i]);\n                                    }\n                                }\n                            }\n                        }\n\n                        for(i = 0; i < preEvalVisitors.length; i++) {\n                            preEvalVisitors[i].run(root);\n                        }\n\n                        evaldRoot = evaluate.call(root, evalEnv);\n\n                        for(i = 0; i < visitors.length; i++) {\n                            visitors[i].run(evaldRoot);\n                        }\n\n                        if (options.sourceMap) {\n                            evaldRoot = new tree.sourceMapOutput(\n                                {\n                                    contentsIgnoredCharsMap: parser.imports.contentsIgnoredChars,\n                                    writeSourceMap: options.writeSourceMap,\n                                    rootNode: evaldRoot,\n                                    contentsMap: parser.imports.contents,\n                                    sourceMapFilename: options.sourceMapFilename,\n                                    sourceMapURL: options.sourceMapURL,\n                                    outputFilename: options.sourceMapOutputFilename,\n                                    sourceMapBasepath: options.sourceMapBasepath,\n                                    sourceMapRootpath: options.sourceMapRootpath,\n                                    outputSourceFiles: options.outputSourceFiles,\n                                    sourceMapGenerator: options.sourceMapGenerator\n                                });\n                        }\n\n                        css = evaldRoot.toCSS({\n                                compress: Boolean(options.compress),\n                                dumpLineNumbers: env.dumpLineNumbers,\n                                strictUnits: Boolean(options.strictUnits),\n                                numPrecision: 8});\n                    } catch (e) {\n                        throw new(LessError)(e, env);\n                    }\n\n                    if (options.cleancss && less.mode === 'node') {\n                        var CleanCSS = require('clean-css'),\n                            cleancssOptions = options.cleancssOptions || {};\n\n                        if (cleancssOptions.keepSpecialComments === undefined) {\n                            cleancssOptions.keepSpecialComments = \"*\";\n                        }\n                        cleancssOptions.processImport = false;\n                        cleancssOptions.noRebase = true;\n                        if (cleancssOptions.noAdvanced === undefined) {\n                            cleancssOptions.noAdvanced = true;\n                        }\n\n                        return new CleanCSS(cleancssOptions).minify(css);\n                    } else if (options.compress) {\n                        return css.replace(/(^(\\s)+)|((\\s)+$)/g, \"\");\n                    } else {\n                        return css;\n                    }\n                };\n            })(root.eval);\n\n            // If `i` is smaller than the `input.length - 1`,\n            // it means the parser wasn't able to parse the whole\n            // string, so we've got a parsing error.\n            //\n            // We try to extract a \\n delimited string,\n            // showing the line where the parse error occured.\n            // We split it up into two parts (the part which parsed,\n            // and the part which didn't), so we can color them differently.\n            if (i < input.length - 1) {\n                i = furthest;\n                var loc = getLocation(i, input);\n                lines = input.split('\\n');\n                line = loc.line + 1;\n\n                error = {\n                    type: \"Parse\",\n                    message: \"Unrecognised input\",\n                    index: i,\n                    filename: env.currentFileInfo.filename,\n                    line: line,\n                    column: loc.column,\n                    extract: [\n                        lines[line - 2],\n                        lines[line - 1],\n                        lines[line]\n                    ]\n                };\n            }\n\n            var finish = function (e) {\n                e = error || e || parser.imports.error;\n\n                if (e) {\n                    if (!(e instanceof LessError)) {\n                        e = new(LessError)(e, env);\n                    }\n\n                    return callback(e);\n                }\n                else {\n                    return callback(null, root);\n                }\n            };\n\n            if (env.processImports !== false) {\n                new tree.importVisitor(this.imports, finish)\n                    .run(root);\n            } else {\n                return finish();\n            }\n        },\n\n        //\n        // Here in, the parsing rules/functions\n        //\n        // The basic structure of the syntax tree generated is as follows:\n        //\n        //   Ruleset ->  Rule -> Value -> Expression -> Entity\n        //\n        // Here's some LESS code:\n        //\n        //    .class {\n        //      color: #fff;\n        //      border: 1px solid #000;\n        //      width: @w + 4px;\n        //      > .child {...}\n        //    }\n        //\n        // And here's what the parse tree might look like:\n        //\n        //     Ruleset (Selector '.class', [\n        //         Rule (\"color\",  Value ([Expression [Color #fff]]))\n        //         Rule (\"border\", Value ([Expression [Dimension 1px][Keyword \"solid\"][Color #000]]))\n        //         Rule (\"width\",  Value ([Expression [Operation \"+\" [Variable \"@w\"][Dimension 4px]]]))\n        //         Ruleset (Selector [Element '>', '.child'], [...])\n        //     ])\n        //\n        //  In general, most rules will try to parse a token with the `$()` function, and if the return\n        //  value is truly, will return a new node, of the relevant type. Sometimes, we need to check\n        //  first, before parsing, that's when we use `peek()`.\n        //\n        parsers: parsers = {\n            //\n            // The `primary` rule is the *entry* and *exit* point of the parser.\n            // The rules here can appear at any level of the parse tree.\n            //\n            // The recursive nature of the grammar is an interplay between the `block`\n            // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,\n            // as represented by this simplified grammar:\n            //\n            //     primary  →  (ruleset | rule)+\n            //     ruleset  →  selector+ block\n            //     block    →  '{' primary '}'\n            //\n            // Only at one point is the primary rule not called from the\n            // block rule: at the root level.\n            //\n            primary: function () {\n                var mixin = this.mixin, $re = _$re, root = [], node;\n\n                while (current)\n                {\n                    node = this.extendRule() || mixin.definition() || this.rule() || this.ruleset() ||\n                        mixin.call() || this.comment() || this.rulesetCall() || this.directive();\n                    if (node) {\n                        root.push(node);\n                    } else {\n                        if (!($re(/^[\\s\\n]+/) || $re(/^;+/))) {\n                            break;\n                        }\n                    }\n                    if (peekChar('}')) {\n                        break;\n                    }\n                }\n\n                return root;\n            },\n\n            // We create a Comment node for CSS comments `/* */`,\n            // but keep the LeSS comments `//` silent, by just skipping\n            // over them.\n            comment: function () {\n                var comment;\n\n                if (input.charAt(i) !== '/') { return; }\n\n                if (input.charAt(i + 1) === '/') {\n                    return new(tree.Comment)($re(/^\\/\\/.*/), true, i, env.currentFileInfo);\n                }\n                comment = $re(/^\\/\\*(?:[^*]|\\*+[^\\/*])*\\*+\\/\\n?/);\n                if (comment) {\n                    return new(tree.Comment)(comment, false, i, env.currentFileInfo);\n                }\n            },\n\n            comments: function () {\n                var comment, comments = [];\n\n                while(true) {\n                    comment = this.comment();\n                    if (!comment) {\n                        break;\n                    }\n                    comments.push(comment);\n                }\n\n                return comments;\n            },\n\n            //\n            // Entities are tokens which can be found inside an Expression\n            //\n            entities: {\n                //\n                // A string, which supports escaping \" and '\n                //\n                //     \"milky way\" 'he\\'s the one!'\n                //\n                quoted: function () {\n                    var str, j = i, e, index = i;\n\n                    if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings\n                    if (input.charAt(j) !== '\"' && input.charAt(j) !== \"'\") { return; }\n\n                    if (e) { $char('~'); }\n\n                    str = $re(/^\"((?:[^\"\\\\\\r\\n]|\\\\.)*)\"|'((?:[^'\\\\\\r\\n]|\\\\.)*)'/);\n                    if (str) {\n                        return new(tree.Quoted)(str[0], str[1] || str[2], e, index, env.currentFileInfo);\n                    }\n                },\n\n                //\n                // A catch-all word, such as:\n                //\n                //     black border-collapse\n                //\n                keyword: function () {\n                    var k;\n\n                    k = $re(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/);\n                    if (k) {\n                        var color = tree.Color.fromKeyword(k);\n                        if (color) {\n                            return color;\n                        }\n                        return new(tree.Keyword)(k);\n                    }\n                },\n\n                //\n                // A function call\n                //\n                //     rgb(255, 0, 255)\n                //\n                // We also try to catch IE's `alpha()`, but let the `alpha` parser\n                // deal with the details.\n                //\n                // The arguments are parsed with the `entities.arguments` parser.\n                //\n                call: function () {\n                    var name, nameLC, args, alpha_ret, index = i;\n\n                    name = /^([\\w-]+|%|progid:[\\w\\.]+)\\(/.exec(current);\n                    if (!name) { return; }\n\n                    name = name[1];\n                    nameLC = name.toLowerCase();\n                    if (nameLC === 'url') {\n                        return null;\n                    }\n\n                    i += name.length;\n\n                    if (nameLC === 'alpha') {\n                        alpha_ret = parsers.alpha();\n                        if(typeof alpha_ret !== 'undefined') {\n                            return alpha_ret;\n                        }\n                    }\n\n                    $char('('); // Parse the '(' and consume whitespace.\n\n                    args = this.arguments();\n\n                    if (! $char(')')) {\n                        return;\n                    }\n\n                    if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); }\n                },\n                arguments: function () {\n                    var args = [], arg;\n\n                    while (true) {\n                        arg = this.assignment() || parsers.expression();\n                        if (!arg) {\n                            break;\n                        }\n                        args.push(arg);\n                        if (! $char(',')) {\n                            break;\n                        }\n                    }\n                    return args;\n                },\n                literal: function () {\n                    return this.dimension() ||\n                           this.color() ||\n                           this.quoted() ||\n                           this.unicodeDescriptor();\n                },\n\n                // Assignments are argument entities for calls.\n                // They are present in ie filter properties as shown below.\n                //\n                //     filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )\n                //\n\n                assignment: function () {\n                    var key, value;\n                    key = $re(/^\\w+(?=\\s?=)/i);\n                    if (!key) {\n                        return;\n                    }\n                    if (!$char('=')) {\n                        return;\n                    }\n                    value = parsers.entity();\n                    if (value) {\n                        return new(tree.Assignment)(key, value);\n                    }\n                },\n\n                //\n                // Parse url() tokens\n                //\n                // We use a specific rule for urls, because they don't really behave like\n                // standard function calls. The difference is that the argument doesn't have\n                // to be enclosed within a string, so it can't be parsed as an Expression.\n                //\n                url: function () {\n                    var value;\n\n                    if (input.charAt(i) !== 'u' || !$re(/^url\\(/)) {\n                        return;\n                    }\n\n                    value = this.quoted() || this.variable() ||\n                            $re(/^(?:(?:\\\\[\\(\\)'\"])|[^\\(\\)'\"])+/) || \"\";\n\n                    expectChar(')');\n\n                    return new(tree.URL)((value.value != null || value instanceof tree.Variable)\n                                        ? value : new(tree.Anonymous)(value), env.currentFileInfo);\n                },\n\n                //\n                // A Variable entity, such as `@fink`, in\n                //\n                //     width: @fink + 2px\n                //\n                // We use a different parser for variable definitions,\n                // see `parsers.variable`.\n                //\n                variable: function () {\n                    var name, index = i;\n\n                    if (input.charAt(i) === '@' && (name = $re(/^@@?[\\w-]+/))) {\n                        return new(tree.Variable)(name, index, env.currentFileInfo);\n                    }\n                },\n\n                // A variable entity useing the protective {} e.g. @{var}\n                variableCurly: function () {\n                    var curly, index = i;\n\n                    if (input.charAt(i) === '@' && (curly = $re(/^@\\{([\\w-]+)\\}/))) {\n                        return new(tree.Variable)(\"@\" + curly[1], index, env.currentFileInfo);\n                    }\n                },\n\n                //\n                // A Hexadecimal color\n                //\n                //     #4F3C2F\n                //\n                // `rgb` and `hsl` colors are parsed through the `entities.call` parser.\n                //\n                color: function () {\n                    var rgb;\n\n                    if (input.charAt(i) === '#' && (rgb = $re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {\n                        return new(tree.Color)(rgb[1]);\n                    }\n                },\n\n                //\n                // A Dimension, that is, a number and a unit\n                //\n                //     0.5em 95%\n                //\n                dimension: function () {\n                    var value, c = input.charCodeAt(i);\n                    //Is the first char of the dimension 0-9, '.', '+' or '-'\n                    if ((c > 57 || c < 43) || c === 47 || c == 44) {\n                        return;\n                    }\n\n                    value = $re(/^([+-]?\\d*\\.?\\d+)(%|[a-z]+)?/);\n                    if (value) {\n                        return new(tree.Dimension)(value[1], value[2]);\n                    }\n                },\n\n                //\n                // A unicode descriptor, as is used in unicode-range\n                //\n                // U+0??  or U+00A1-00A9\n                //\n                unicodeDescriptor: function () {\n                    var ud;\n\n                    ud = $re(/^U\\+[0-9a-fA-F?]+(\\-[0-9a-fA-F?]+)?/);\n                    if (ud) {\n                        return new(tree.UnicodeDescriptor)(ud[0]);\n                    }\n                },\n\n                //\n                // JavaScript code to be evaluated\n                //\n                //     `window.location.href`\n                //\n                javascript: function () {\n                    var str, j = i, e;\n\n                    if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings\n                    if (input.charAt(j) !== '`') { return; }\n                    if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) {\n                        error(\"You are using JavaScript, which has been disabled.\");\n                    }\n\n                    if (e) { $char('~'); }\n\n                    str = $re(/^`([^`]*)`/);\n                    if (str) {\n                        return new(tree.JavaScript)(str[1], i, e);\n                    }\n                }\n            },\n\n            //\n            // The variable part of a variable definition. Used in the `rule` parser\n            //\n            //     @fink:\n            //\n            variable: function () {\n                var name;\n\n                if (input.charAt(i) === '@' && (name = $re(/^(@[\\w-]+)\\s*:/))) { return name[1]; }\n            },\n\n            //\n            // The variable part of a variable definition. Used in the `rule` parser\n            //\n            //     @fink();\n            //\n            rulesetCall: function () {\n                var name;\n\n                if (input.charAt(i) === '@' && (name = $re(/^(@[\\w-]+)\\s*\\(\\s*\\)\\s*;/))) { \n                    return new tree.RulesetCall(name[1]); \n                }\n            },\n\n            //\n            // extend syntax - used to extend selectors\n            //\n            extend: function(isRule) {\n                var elements, e, index = i, option, extendList, extend;\n\n                if (!(isRule ? $re(/^&:extend\\(/) : $re(/^:extend\\(/))) { return; }\n\n                do {\n                    option = null;\n                    elements = null;\n                    while (! (option = $re(/^(all)(?=\\s*(\\)|,))/))) {\n                        e = this.element();\n                        if (!e) { break; }\n                        if (elements) { elements.push(e); } else { elements = [ e ]; }\n                    }\n\n                    option = option && option[1];\n\n                    extend = new(tree.Extend)(new(tree.Selector)(elements), option, index);\n                    if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }\n\n                } while($char(\",\"));\n                \n                expect(/^\\)/);\n\n                if (isRule) {\n                    expect(/^;/);\n                }\n\n                return extendList;\n            },\n\n            //\n            // extendRule - used in a rule to extend all the parent selectors\n            //\n            extendRule: function() {\n                return this.extend(true);\n            },\n            \n            //\n            // Mixins\n            //\n            mixin: {\n                //\n                // A Mixin call, with an optional argument list\n                //\n                //     #mixins > .square(#fff);\n                //     .rounded(4px, black);\n                //     .button;\n                //\n                // The `while` loop is there because mixins can be\n                // namespaced, but we only support the child and descendant\n                // selector for now.\n                //\n                call: function () {\n                    var s = input.charAt(i), important = false, index = i, elemIndex,\n                        elements, elem, e, c, args;\n\n                    if (s !== '.' && s !== '#') { return; }\n\n                    save(); // stop us absorbing part of an invalid selector\n\n                    while (true) {\n                        elemIndex = i;\n                        e = $re(/^[#.](?:[\\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/);\n                        if (!e) {\n                            break;\n                        }\n                        elem = new(tree.Element)(c, e, elemIndex, env.currentFileInfo);\n                        if (elements) { elements.push(elem); } else { elements = [ elem ]; }\n                        c = $char('>');\n                    }\n\n                    if (elements) {\n                        if ($char('(')) {\n                            args = this.args(true).args;\n                            expectChar(')');\n                        }\n\n                        if (parsers.important()) {\n                            important = true;\n                        }\n\n                        if (parsers.end()) {\n                            forget();\n                            return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important);\n                        }\n                    }\n\n                    restore();\n                },\n                args: function (isCall) {\n                    var parsers = parser.parsers, entities = parsers.entities,\n                        returner = { args:null, variadic: false },\n                        expressions = [], argsSemiColon = [], argsComma = [],\n                        isSemiColonSeperated, expressionContainsNamed, name, nameLoop, value, arg;\n\n                    save();\n\n                    while (true) {\n                        if (isCall) {\n                            arg = parsers.detachedRuleset() || parsers.expression();\n                        } else {\n                            parsers.comments();\n                            if (input.charAt(i) === '.' && $re(/^\\.{3}/)) {\n                                returner.variadic = true;\n                                if ($char(\";\") && !isSemiColonSeperated) {\n                                    isSemiColonSeperated = true;\n                                }\n                                (isSemiColonSeperated ? argsSemiColon : argsComma)\n                                    .push({ variadic: true });\n                                break;\n                            }\n                            arg = entities.variable() || entities.literal() || entities.keyword();\n                        }\n\n                        if (!arg) {\n                            break;\n                        }\n\n                        nameLoop = null;\n                        if (arg.throwAwayComments) {\n                            arg.throwAwayComments();\n                        }\n                        value = arg;\n                        var val = null;\n\n                        if (isCall) {\n                            // Variable\n                            if (arg.value && arg.value.length == 1) {\n                                val = arg.value[0];\n                            }\n                        } else {\n                            val = arg;\n                        }\n\n                        if (val && val instanceof tree.Variable) {\n                            if ($char(':')) {\n                                if (expressions.length > 0) {\n                                    if (isSemiColonSeperated) {\n                                        error(\"Cannot mix ; and , as delimiter types\");\n                                    }\n                                    expressionContainsNamed = true;\n                                }\n\n                                // we do not support setting a ruleset as a default variable - it doesn't make sense\n                                // However if we do want to add it, there is nothing blocking it, just don't error\n                                // and remove isCall dependency below\n                                value = (isCall && parsers.detachedRuleset()) || parsers.expression();\n\n                                if (!value) {\n                                    if (isCall) {\n                                        error(\"could not understand value for named argument\");\n                                    } else {\n                                        restore();\n                                        returner.args = [];\n                                        return returner;\n                                    }\n                                }\n                                nameLoop = (name = val.name);\n                            } else if (!isCall && $re(/^\\.{3}/)) {\n                                returner.variadic = true;\n                                if ($char(\";\") && !isSemiColonSeperated) {\n                                    isSemiColonSeperated = true;\n                                }\n                                (isSemiColonSeperated ? argsSemiColon : argsComma)\n                                    .push({ name: arg.name, variadic: true });\n                                break;\n                            } else if (!isCall) {\n                                name = nameLoop = val.name;\n                                value = null;\n                            }\n                        }\n\n                        if (value) {\n                            expressions.push(value);\n                        }\n\n                        argsComma.push({ name:nameLoop, value:value });\n\n                        if ($char(',')) {\n                            continue;\n                        }\n\n                        if ($char(';') || isSemiColonSeperated) {\n\n                            if (expressionContainsNamed) {\n                                error(\"Cannot mix ; and , as delimiter types\");\n                            }\n\n                            isSemiColonSeperated = true;\n\n                            if (expressions.length > 1) {\n                                value = new(tree.Value)(expressions);\n                            }\n                            argsSemiColon.push({ name:name, value:value });\n\n                            name = null;\n                            expressions = [];\n                            expressionContainsNamed = false;\n                        }\n                    }\n\n                    forget();\n                    returner.args = isSemiColonSeperated ? argsSemiColon : argsComma;\n                    return returner;\n                },\n                //\n                // A Mixin definition, with a list of parameters\n                //\n                //     .rounded (@radius: 2px, @color) {\n                //        ...\n                //     }\n                //\n                // Until we have a finer grained state-machine, we have to\n                // do a look-ahead, to make sure we don't have a mixin call.\n                // See the `rule` function for more information.\n                //\n                // We start by matching `.rounded (`, and then proceed on to\n                // the argument list, which has optional default values.\n                // We store the parameters in `params`, with a `value` key,\n                // if there is a value, such as in the case of `@radius`.\n                //\n                // Once we've got our params list, and a closing `)`, we parse\n                // the `{...}` block.\n                //\n                definition: function () {\n                    var name, params = [], match, ruleset, cond, variadic = false;\n                    if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') ||\n                        peek(/^[^{]*\\}/)) {\n                        return;\n                    }\n\n                    save();\n\n                    match = $re(/^([#.](?:[\\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\\s*\\(/);\n                    if (match) {\n                        name = match[1];\n\n                        var argInfo = this.args(false);\n                        params = argInfo.args;\n                        variadic = argInfo.variadic;\n\n                        // .mixincall(\"@{a}\");\n                        // looks a bit like a mixin definition.. \n                        // also\n                        // .mixincall(@a: {rule: set;});\n                        // so we have to be nice and restore\n                        if (!$char(')')) {\n                            furthest = i;\n                            restore();\n                            return;\n                        }\n                        \n                        parsers.comments();\n\n                        if ($re(/^when/)) { // Guard\n                            cond = expect(parsers.conditions, 'expected condition');\n                        }\n\n                        ruleset = parsers.block();\n\n                        if (ruleset) {\n                            forget();\n                            return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic);\n                        } else {\n                            restore();\n                        }\n                    } else {\n                        forget();\n                    }\n                }\n            },\n\n            //\n            // Entities are the smallest recognized token,\n            // and can be found inside a rule's value.\n            //\n            entity: function () {\n                var entities = this.entities;\n\n                return entities.literal() || entities.variable() || entities.url() ||\n                       entities.call()    || entities.keyword()  || entities.javascript() ||\n                       this.comment();\n            },\n\n            //\n            // A Rule terminator. Note that we use `peek()` to check for '}',\n            // because the `block` rule will be expecting it, but we still need to make sure\n            // it's there, if ';' was ommitted.\n            //\n            end: function () {\n                return $char(';') || peekChar('}');\n            },\n\n            //\n            // IE's alpha function\n            //\n            //     alpha(opacity=88)\n            //\n            alpha: function () {\n                var value;\n\n                if (! $re(/^\\(opacity=/i)) { return; }\n                value = $re(/^\\d+/) || this.entities.variable();\n                if (value) {\n                    expectChar(')');\n                    return new(tree.Alpha)(value);\n                }\n            },\n\n            //\n            // A Selector Element\n            //\n            //     div\n            //     + h1\n            //     #socks\n            //     input[type=\"text\"]\n            //\n            // Elements are the building blocks for Selectors,\n            // they are made out of a `Combinator` (see combinator rule),\n            // and an element name, such as a tag a class, or `*`.\n            //\n            element: function () {\n                var e, c, v, index = i;\n\n                c = this.combinator();\n\n                e = $re(/^(?:\\d+\\.\\d+|\\d+)%/) || $re(/^(?:[.#]?|:*)(?:[\\w-]|[^\\x00-\\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||\n                    $char('*') || $char('&') || this.attribute() || $re(/^\\([^()@]+\\)/) || $re(/^[\\.#](?=@)/) ||\n                    this.entities.variableCurly();\n\n                if (! e) {\n                    save();\n                    if ($char('(')) {\n                        if ((v = this.selector()) && $char(')')) {\n                            e = new(tree.Paren)(v);\n                            forget();\n                        } else {\n                            restore();\n                        }\n                    } else {\n                        forget();\n                    }\n                }\n\n                if (e) { return new(tree.Element)(c, e, index, env.currentFileInfo); }\n            },\n\n            //\n            // Combinators combine elements together, in a Selector.\n            //\n            // Because our parser isn't white-space sensitive, special care\n            // has to be taken, when parsing the descendant combinator, ` `,\n            // as it's an empty space. We have to check the previous character\n            // in the input, to see if it's a ` ` character. More info on how\n            // we deal with this in *combinator.js*.\n            //\n            combinator: function () {\n                var c = input.charAt(i);\n                \n                if (c === '>' || c === '+' || c === '~' || c === '|' || c === '^') {\n                    i++;\n                    if (input.charAt(i) === '^') {\n                        c = '^^';\n                        i++;\n                    }\n                    while (isWhitespace(input, i)) { i++; }\n                    return new(tree.Combinator)(c);\n                } else if (isWhitespace(input, i - 1)) {\n                    return new(tree.Combinator)(\" \");\n                } else {\n                    return new(tree.Combinator)(null);\n                }\n            },\n            //\n            // A CSS selector (see selector below)\n            // with less extensions e.g. the ability to extend and guard\n            //\n            lessSelector: function () {\n                return this.selector(true);\n            },\n            //\n            // A CSS Selector\n            //\n            //     .class > div + h1\n            //     li a:hover\n            //\n            // Selectors are made out of one or more Elements, see above.\n            //\n            selector: function (isLess) {\n                var index = i, $re = _$re, elements, extendList, c, e, extend, when, condition;\n\n                while ((isLess && (extend = this.extend())) || (isLess && (when = $re(/^when/))) || (e = this.element())) {\n                    if (when) {\n                        condition = expect(this.conditions, 'expected condition');\n                    } else if (condition) {\n                        error(\"CSS guard can only be used at the end of selector\");\n                    } else if (extend) {\n                        if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }\n                    } else {\n                        if (extendList) { error(\"Extend can only be used at the end of selector\"); }\n                        c = input.charAt(i);\n                        if (elements) { elements.push(e); } else { elements = [ e ]; }\n                        e = null;\n                    }\n                    if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') {\n                        break;\n                    }\n                }\n\n                if (elements) { return new(tree.Selector)(elements, extendList, condition, index, env.currentFileInfo); }\n                if (extendList) { error(\"Extend must be used to extend a selector, it cannot be used on its own\"); }\n            },\n            attribute: function () {\n                if (! $char('[')) { return; }\n\n                var entities = this.entities,\n                    key, val, op;\n\n                if (!(key = entities.variableCurly())) {\n                    key = expect(/^(?:[_A-Za-z0-9-\\*]*\\|)?(?:[_A-Za-z0-9-]|\\\\.)+/);\n                }\n\n                op = $re(/^[|~*$^]?=/);\n                if (op) {\n                    val = entities.quoted() || $re(/^[0-9]+%/) || $re(/^[\\w-]+/) || entities.variableCurly();\n                }\n\n                expectChar(']');\n\n                return new(tree.Attribute)(key, op, val);\n            },\n\n            //\n            // The `block` rule is used by `ruleset` and `mixin.definition`.\n            // It's a wrapper around the `primary` rule, with added `{}`.\n            //\n            block: function () {\n                var content;\n                if ($char('{') && (content = this.primary()) && $char('}')) {\n                    return content;\n                }\n            },\n\n            blockRuleset: function() {\n                var block = this.block();\n\n                if (block) {\n                    block = new tree.Ruleset(null, block);\n                }\n                return block;\n            },\n            \n            detachedRuleset: function() {\n                var blockRuleset = this.blockRuleset();\n                if (blockRuleset) {\n                    return new tree.DetachedRuleset(blockRuleset);\n                }\n            },\n\n            //\n            // div, .class, body > p {...}\n            //\n            ruleset: function () {\n                var selectors, s, rules, debugInfo;\n                \n                save();\n\n                if (env.dumpLineNumbers) {\n                    debugInfo = getDebugInfo(i, input, env);\n                }\n\n                while (true) {\n                    s = this.lessSelector();\n                    if (!s) {\n                        break;\n                    }\n                    if (selectors) { selectors.push(s); } else { selectors = [ s ]; }\n                    this.comments();\n                    if (s.condition && selectors.length > 1) {\n                        error(\"Guards are only currently allowed on a single selector.\");\n                    }\n                    if (! $char(',')) { break; }\n                    if (s.condition) {\n                        error(\"Guards are only currently allowed on a single selector.\");\n                    }\n                    this.comments();\n                }\n\n                if (selectors && (rules = this.block())) {\n                    forget();\n                    var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports);\n                    if (env.dumpLineNumbers) {\n                        ruleset.debugInfo = debugInfo;\n                    }\n                    return ruleset;\n                } else {\n                    // Backtrack\n                    furthest = i;\n                    restore();\n                }\n            },\n            rule: function (tryAnonymous) {\n                var name, value, startOfRule = i, c = input.charAt(startOfRule), important, merge, isVariable;\n\n                if (c === '.' || c === '#' || c === '&') { return; }\n\n                save();\n\n                name = this.variable() || this.ruleProperty();\n                if (name) {\n                    isVariable = typeof name === \"string\";\n                    \n                    if (isVariable) {\n                        value = this.detachedRuleset();\n                    }\n                    \n                    if (!value) {\n                        // prefer to try to parse first if its a variable or we are compressing\n                        // but always fallback on the other one\n                        value = !tryAnonymous && (env.compress || isVariable) ?\n                            (this.value() || this.anonymousValue()) :\n                            (this.anonymousValue() || this.value());\n    \n                        important = this.important();\n                        \n                        // a name returned by this.ruleProperty() is always an array of the form:\n                        // [string-1, ..., string-n, \"\"] or [string-1, ..., string-n, \"+\"]\n                        // where each item is a tree.Keyword or tree.Variable\n                        merge = !isVariable && name.pop().value;\n                    }\n\n                    if (value && this.end()) {\n                        forget();\n                        return new (tree.Rule)(name, value, important, merge, startOfRule, env.currentFileInfo);\n                    } else {\n                        furthest = i;\n                        restore();\n                        if (value && !tryAnonymous) {\n                            return this.rule(true);\n                        }\n                    }\n                } else {\n                    forget();\n                }\n            },\n            anonymousValue: function () {\n                var match;\n                match = /^([^@+\\/'\"*`(;{}-]*);/.exec(current);\n                if (match) {\n                    i += match[0].length - 1;\n                    return new(tree.Anonymous)(match[1]);\n                }\n            },\n\n            //\n            // An @import directive\n            //\n            //     @import \"lib\";\n            //\n            // Depending on our environemnt, importing is done differently:\n            // In the browser, it's an XHR request, in Node, it would be a\n            // file-system operation. The function used for importing is\n            // stored in `import`, which we pass to the Import constructor.\n            //\n            \"import\": function () {\n                var path, features, index = i;\n\n                save();\n\n                var dir = $re(/^@import?\\s+/);\n\n                var options = (dir ? this.importOptions() : null) || {};\n\n                if (dir && (path = this.entities.quoted() || this.entities.url())) {\n                    features = this.mediaFeatures();\n                    if ($char(';')) {\n                        forget();\n                        features = features && new(tree.Value)(features);\n                        return new(tree.Import)(path, features, options, index, env.currentFileInfo);\n                    }\n                }\n\n                restore();\n            },\n\n            importOptions: function() {\n                var o, options = {}, optionName, value;\n\n                // list of options, surrounded by parens\n                if (! $char('(')) { return null; }\n                do {\n                    o = this.importOption();\n                    if (o) {\n                        optionName = o;\n                        value = true;\n                        switch(optionName) {\n                            case \"css\":\n                                optionName = \"less\";\n                                value = false;\n                            break;\n                            case \"once\":\n                                optionName = \"multiple\";\n                                value = false;\n                            break;\n                        }\n                        options[optionName] = value;\n                        if (! $char(',')) { break; }\n                    }\n                } while (o);\n                expectChar(')');\n                return options;\n            },\n\n            importOption: function() {\n                var opt = $re(/^(less|css|multiple|once|inline|reference)/);\n                if (opt) {\n                    return opt[1];\n                }\n            },\n\n            mediaFeature: function () {\n                var entities = this.entities, nodes = [], e, p;\n                do {\n                    e = entities.keyword() || entities.variable();\n                    if (e) {\n                        nodes.push(e);\n                    } else if ($char('(')) {\n                        p = this.property();\n                        e = this.value();\n                        if ($char(')')) {\n                            if (p && e) {\n                                nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, null, i, env.currentFileInfo, true)));\n                            } else if (e) {\n                                nodes.push(new(tree.Paren)(e));\n                            } else {\n                                return null;\n                            }\n                        } else { return null; }\n                    }\n                } while (e);\n\n                if (nodes.length > 0) {\n                    return new(tree.Expression)(nodes);\n                }\n            },\n\n            mediaFeatures: function () {\n                var entities = this.entities, features = [], e;\n                do {\n                    e = this.mediaFeature();\n                    if (e) {\n                        features.push(e);\n                        if (! $char(',')) { break; }\n                    } else {\n                        e = entities.variable();\n                        if (e) {\n                            features.push(e);\n                            if (! $char(',')) { break; }\n                        }\n                    }\n                } while (e);\n\n                return features.length > 0 ? features : null;\n            },\n\n            media: function () {\n                var features, rules, media, debugInfo;\n\n                if (env.dumpLineNumbers) {\n                    debugInfo = getDebugInfo(i, input, env);\n                }\n\n                if ($re(/^@media/)) {\n                    features = this.mediaFeatures();\n\n                    rules = this.block();\n                    if (rules) {\n                        media = new(tree.Media)(rules, features, i, env.currentFileInfo);\n                        if (env.dumpLineNumbers) {\n                            media.debugInfo = debugInfo;\n                        }\n                        return media;\n                    }\n                }\n            },\n\n            //\n            // A CSS Directive\n            //\n            //     @charset \"utf-8\";\n            //\n            directive: function () {\n                var index = i, name, value, rules, nonVendorSpecificName,\n                    hasIdentifier, hasExpression, hasUnknown, hasBlock = true;\n\n                if (input.charAt(i) !== '@') { return; }\n\n                value = this['import']() || this.media();\n                if (value) {\n                    return value;\n                }\n\n                save();\n\n                name = $re(/^@[a-z-]+/);\n                \n                if (!name) { return; }\n\n                nonVendorSpecificName = name;\n                if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) {\n                    nonVendorSpecificName = \"@\" + name.slice(name.indexOf('-', 2) + 1);\n                }\n\n                switch(nonVendorSpecificName) {\n                    /*\n                    case \"@font-face\":\n                    case \"@viewport\":\n                    case \"@top-left\":\n                    case \"@top-left-corner\":\n                    case \"@top-center\":\n                    case \"@top-right\":\n                    case \"@top-right-corner\":\n                    case \"@bottom-left\":\n                    case \"@bottom-left-corner\":\n                    case \"@bottom-center\":\n                    case \"@bottom-right\":\n                    case \"@bottom-right-corner\":\n                    case \"@left-top\":\n                    case \"@left-middle\":\n                    case \"@left-bottom\":\n                    case \"@right-top\":\n                    case \"@right-middle\":\n                    case \"@right-bottom\":\n                        hasBlock = true;\n                        break;\n                    */\n                    case \"@charset\":\n                        hasIdentifier = true;\n                        hasBlock = false;\n                        break;\n                    case \"@namespace\":\n                        hasExpression = true;\n                        hasBlock = false;\n                        break;\n                    case \"@keyframes\":\n                        hasIdentifier = true;\n                        break;\n                    case \"@host\":\n                    case \"@page\":\n                    case \"@document\":\n                    case \"@supports\":\n                        hasUnknown = true;\n                        break;\n                }\n\n                if (hasIdentifier) {\n                    value = this.entity();\n                    if (!value) {\n                        error(\"expected \" + name + \" identifier\");\n                    }\n                } else if (hasExpression) {\n                    value = this.expression();\n                    if (!value) {\n                        error(\"expected \" + name + \" expression\");\n                    }\n                } else if (hasUnknown) {\n                    value = ($re(/^[^{;]+/) || '').trim();\n                    if (value) {\n                        value = new(tree.Anonymous)(value);\n                    }\n                }\n\n                if (hasBlock) {\n                    rules = this.blockRuleset();\n                }\n\n                if (rules || (!hasBlock && value && $char(';'))) {\n                    forget();\n                    return new(tree.Directive)(name, value, rules, index, env.currentFileInfo, \n                        env.dumpLineNumbers ? getDebugInfo(index, input, env) : null);\n                }\n\n                restore();\n            },\n\n            //\n            // A Value is a comma-delimited list of Expressions\n            //\n            //     font-family: Baskerville, Georgia, serif;\n            //\n            // In a Rule, a Value represents everything after the `:`,\n            // and before the `;`.\n            //\n            value: function () {\n                var e, expressions = [];\n\n                do {\n                    e = this.expression();\n                    if (e) {\n                        expressions.push(e);\n                        if (! $char(',')) { break; }\n                    }\n                } while(e);\n\n                if (expressions.length > 0) {\n                    return new(tree.Value)(expressions);\n                }\n            },\n            important: function () {\n                if (input.charAt(i) === '!') {\n                    return $re(/^! *important/);\n                }\n            },\n            sub: function () {\n                var a, e;\n\n                if ($char('(')) {\n                    a = this.addition();\n                    if (a) {\n                        e = new(tree.Expression)([a]);\n                        expectChar(')');\n                        e.parens = true;\n                        return e;\n                    }\n                }\n            },\n            multiplication: function () {\n                var m, a, op, operation, isSpaced;\n                m = this.operand();\n                if (m) {\n                    isSpaced = isWhitespace(input, i - 1);\n                    while (true) {\n                        if (peek(/^\\/[*\\/]/)) {\n                            break;\n                        }\n                        op = $char('/') || $char('*');\n\n                        if (!op) { break; }\n\n                        a = this.operand();\n\n                        if (!a) { break; }\n\n                        m.parensInOp = true;\n                        a.parensInOp = true;\n                        operation = new(tree.Operation)(op, [operation || m, a], isSpaced);\n                        isSpaced = isWhitespace(input, i - 1);\n                    }\n                    return operation || m;\n                }\n            },\n            addition: function () {\n                var m, a, op, operation, isSpaced;\n                m = this.multiplication();\n                if (m) {\n                    isSpaced = isWhitespace(input, i - 1);\n                    while (true) {\n                        op = $re(/^[-+]\\s+/) || (!isSpaced && ($char('+') || $char('-')));\n                        if (!op) {\n                            break;\n                        }\n                        a = this.multiplication();\n                        if (!a) {\n                            break;\n                        }\n                        \n                        m.parensInOp = true;\n                        a.parensInOp = true;\n                        operation = new(tree.Operation)(op, [operation || m, a], isSpaced);\n                        isSpaced = isWhitespace(input, i - 1);\n                    }\n                    return operation || m;\n                }\n            },\n            conditions: function () {\n                var a, b, index = i, condition;\n\n                a = this.condition();\n                if (a) {\n                    while (true) {\n                        if (!peek(/^,\\s*(not\\s*)?\\(/) || !$char(',')) {\n                            break;\n                        }\n                        b = this.condition();\n                        if (!b) {\n                            break;\n                        }\n                        condition = new(tree.Condition)('or', condition || a, b, index);\n                    }\n                    return condition || a;\n                }\n            },\n            condition: function () {\n                var entities = this.entities, index = i, negate = false,\n                    a, b, c, op;\n\n                if ($re(/^not/)) { negate = true; }\n                expectChar('(');\n                a = this.addition() || entities.keyword() || entities.quoted();\n                if (a) {\n                    op = $re(/^(?:>=|<=|=<|[<=>])/);\n                    if (op) {\n                        b = this.addition() || entities.keyword() || entities.quoted();\n                        if (b) {\n                            c = new(tree.Condition)(op, a, b, index, negate);\n                        } else {\n                            error('expected expression');\n                        }\n                    } else {\n                        c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate);\n                    }\n                    expectChar(')');\n                    return $re(/^and/) ? new(tree.Condition)('and', c, this.condition()) : c;\n                }\n            },\n\n            //\n            // An operand is anything that can be part of an operation,\n            // such as a Color, or a Variable\n            //\n            operand: function () {\n                var entities = this.entities,\n                    p = input.charAt(i + 1), negate;\n\n                if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $char('-'); }\n                var o = this.sub() || entities.dimension() ||\n                        entities.color() || entities.variable() ||\n                        entities.call();\n\n                if (negate) {\n                    o.parensInOp = true;\n                    o = new(tree.Negative)(o);\n                }\n\n                return o;\n            },\n\n            //\n            // Expressions either represent mathematical operations,\n            // or white-space delimited Entities.\n            //\n            //     1px solid black\n            //     @var * 2\n            //\n            expression: function () {\n                var entities = [], e, delim;\n\n                do {\n                    e = this.addition() || this.entity();\n                    if (e) {\n                        entities.push(e);\n                        // operations do not allow keyword \"/\" dimension (e.g. small/20px) so we support that here\n                        if (!peek(/^\\/[\\/*]/)) {\n                            delim = $char('/');\n                            if (delim) {\n                                entities.push(new(tree.Anonymous)(delim));\n                            }\n                        }\n                    }\n                } while (e);\n                if (entities.length > 0) {\n                    return new(tree.Expression)(entities);\n                }\n            },\n            property: function () {\n                var name = $re(/^(\\*?-?[_a-zA-Z0-9-]+)\\s*:/);\n                if (name) {\n                    return name[1];\n                }\n            },\n            ruleProperty: function () {\n                var c = current, name = [], index = [], length = 0, s, k;\n                \n                function match(re) {\n                    var a = re.exec(c);\n                    if (a) {\n                        index.push(i + length);\n                        length += a[0].length;\n                        c = c.slice(a[1].length);\n                        return name.push(a[1]);\n                    }\n                }\n\n                match(/^(\\*?)/);\n                while (match(/^((?:[\\w-]+)|(?:@\\{[\\w-]+\\}))/)); // !\n                if ((name.length > 1) && match(/^\\s*((?:\\+_|\\+)?)\\s*:/)) {\n                    // at last, we have the complete match now. move forward, \n                    // convert name particles to tree objects and return:\n                    skipWhitespace(length);\n                    if (name[0] === '') {\n                        name.shift();\n                        index.shift();\n                    }\n                    for (k = 0; k < name.length; k++) {\n                        s = name[k];\n                        name[k] = (s.charAt(0) !== '@')\n                            ? new(tree.Keyword)(s)\n                            : new(tree.Variable)('@' + s.slice(2, -1), \n                                index[k], env.currentFileInfo);\n                    }\n                    return name;\n                }\n            }\n        }\n    };\n    return parser;\n};\nless.Parser.serializeVars = function(vars) {\n    var s = '';\n\n    for (var name in vars) {\n        if (Object.hasOwnProperty.call(vars, name)) {\n            var value = vars[name];\n            s += ((name[0] === '@') ? '' : '@') + name +': '+ value +\n                    ((('' + value).slice(-1) === ';') ? '' : ';');\n        }\n    }\n\n    return s;\n};\n\n(function (tree) {\n\ntree.functions = {\n    rgb: function (r, g, b) {\n        return this.rgba(r, g, b, 1.0);\n    },\n    rgba: function (r, g, b, a) {\n        var rgb = [r, g, b].map(function (c) { return scaled(c, 255); });\n        a = number(a);\n        return new(tree.Color)(rgb, a);\n    },\n    hsl: function (h, s, l) {\n        return this.hsla(h, s, l, 1.0);\n    },\n    hsla: function (h, s, l, a) {\n        function hue(h) {\n            h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);\n            if      (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; }\n            else if (h * 2 < 1) { return m2; }\n            else if (h * 3 < 2) { return m1 + (m2 - m1) * (2/3 - h) * 6; }\n            else                { return m1; }\n        }\n\n        h = (number(h) % 360) / 360;\n        s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a));\n\n        var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;\n        var m1 = l * 2 - m2;\n\n        return this.rgba(hue(h + 1/3) * 255,\n                         hue(h)       * 255,\n                         hue(h - 1/3) * 255,\n                         a);\n    },\n\n    hsv: function(h, s, v) {\n        return this.hsva(h, s, v, 1.0);\n    },\n\n    hsva: function(h, s, v, a) {\n        h = ((number(h) % 360) / 360) * 360;\n        s = number(s); v = number(v); a = number(a);\n\n        var i, f;\n        i = Math.floor((h / 60) % 6);\n        f = (h / 60) - i;\n\n        var vs = [v,\n                  v * (1 - s),\n                  v * (1 - f * s),\n                  v * (1 - (1 - f) * s)];\n        var perm = [[0, 3, 1],\n                    [2, 0, 1],\n                    [1, 0, 3],\n                    [1, 2, 0],\n                    [3, 1, 0],\n                    [0, 1, 2]];\n\n        return this.rgba(vs[perm[i][0]] * 255,\n                         vs[perm[i][1]] * 255,\n                         vs[perm[i][2]] * 255,\n                         a);\n    },\n\n    hue: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().h));\n    },\n    saturation: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%');\n    },\n    lightness: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%');\n    },\n    hsvhue: function(color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().h));\n    },\n    hsvsaturation: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().s * 100), '%');\n    },\n    hsvvalue: function (color) {\n        return new(tree.Dimension)(Math.round(color.toHSV().v * 100), '%');\n    },\n    red: function (color) {\n        return new(tree.Dimension)(color.rgb[0]);\n    },\n    green: function (color) {\n        return new(tree.Dimension)(color.rgb[1]);\n    },\n    blue: function (color) {\n        return new(tree.Dimension)(color.rgb[2]);\n    },\n    alpha: function (color) {\n        return new(tree.Dimension)(color.toHSL().a);\n    },\n    luma: function (color) {\n        return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%');\n    },\n    luminance: function (color) {\n        var luminance =\n            (0.2126 * color.rgb[0] / 255)\n          + (0.7152 * color.rgb[1] / 255)\n          + (0.0722 * color.rgb[2] / 255);\n\n        return new(tree.Dimension)(Math.round(luminance * color.alpha * 100), '%');\n    },\n    saturate: function (color, amount) {\n        // filter: saturate(3.2);\n        // should be kept as is, so check for color\n        if (!color.rgb) {\n            return null;\n        }\n        var hsl = color.toHSL();\n\n        hsl.s += amount.value / 100;\n        hsl.s = clamp(hsl.s);\n        return hsla(hsl);\n    },\n    desaturate: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.s -= amount.value / 100;\n        hsl.s = clamp(hsl.s);\n        return hsla(hsl);\n    },\n    lighten: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.l += amount.value / 100;\n        hsl.l = clamp(hsl.l);\n        return hsla(hsl);\n    },\n    darken: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.l -= amount.value / 100;\n        hsl.l = clamp(hsl.l);\n        return hsla(hsl);\n    },\n    fadein: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a += amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    fadeout: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a -= amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    fade: function (color, amount) {\n        var hsl = color.toHSL();\n\n        hsl.a = amount.value / 100;\n        hsl.a = clamp(hsl.a);\n        return hsla(hsl);\n    },\n    spin: function (color, amount) {\n        var hsl = color.toHSL();\n        var hue = (hsl.h + amount.value) % 360;\n\n        hsl.h = hue < 0 ? 360 + hue : hue;\n\n        return hsla(hsl);\n    },\n    //\n    // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein\n    // http://sass-lang.com\n    //\n    mix: function (color1, color2, weight) {\n        if (!weight) {\n            weight = new(tree.Dimension)(50);\n        }\n        var p = weight.value / 100.0;\n        var w = p * 2 - 1;\n        var a = color1.toHSL().a - color2.toHSL().a;\n\n        var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;\n        var w2 = 1 - w1;\n\n        var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2,\n                   color1.rgb[1] * w1 + color2.rgb[1] * w2,\n                   color1.rgb[2] * w1 + color2.rgb[2] * w2];\n\n        var alpha = color1.alpha * p + color2.alpha * (1 - p);\n\n        return new(tree.Color)(rgb, alpha);\n    },\n    greyscale: function (color) {\n        return this.desaturate(color, new(tree.Dimension)(100));\n    },\n    contrast: function (color, dark, light, threshold) {\n        // filter: contrast(3.2);\n        // should be kept as is, so check for color\n        if (!color.rgb) {\n            return null;\n        }\n        if (typeof light === 'undefined') {\n            light = this.rgba(255, 255, 255, 1.0);\n        }\n        if (typeof dark === 'undefined') {\n            dark = this.rgba(0, 0, 0, 1.0);\n        }\n        //Figure out which is actually light and dark!\n        if (dark.luma() > light.luma()) {\n            var t = light;\n            light = dark;\n            dark = t;\n        }\n        if (typeof threshold === 'undefined') {\n            threshold = 0.43;\n        } else {\n            threshold = number(threshold);\n        }\n        if (color.luma() < threshold) {\n            return light;\n        } else {\n            return dark;\n        }\n    },\n    e: function (str) {\n        return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str);\n    },\n    escape: function (str) {\n        return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, \"%3D\").replace(/:/g, \"%3A\").replace(/#/g, \"%23\").replace(/;/g, \"%3B\").replace(/\\(/g, \"%28\").replace(/\\)/g, \"%29\"));\n    },\n    replace: function (string, pattern, replacement, flags) {\n        var result = string.value;\n\n        result = result.replace(new RegExp(pattern.value, flags ? flags.value : ''), replacement.value);\n        return new(tree.Quoted)(string.quote || '', result, string.escaped);\n    },\n    '%': function (string /* arg, arg, ...*/) {\n        var args = Array.prototype.slice.call(arguments, 1),\n            result = string.value;\n\n        for (var i = 0; i < args.length; i++) {\n            /*jshint loopfunc:true */\n            result = result.replace(/%[sda]/i, function(token) {\n                var value = token.match(/s/i) ? args[i].value : args[i].toCSS();\n                return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value;\n            });\n        }\n        result = result.replace(/%%/g, '%');\n        return new(tree.Quoted)(string.quote || '', result, string.escaped);\n    },\n    unit: function (val, unit) {\n        if(!(val instanceof tree.Dimension)) {\n            throw { type: \"Argument\", message: \"the first argument to unit must be a number\" + (val instanceof tree.Operation ? \". Have you forgotten parenthesis?\" : \"\") };\n        }\n        if (unit) {\n            if (unit instanceof tree.Keyword) {\n                unit = unit.value;\n            } else {\n                unit = unit.toCSS();\n            }\n        } else {\n            unit = \"\";\n        }\n        return new(tree.Dimension)(val.value, unit);\n    },\n    convert: function (val, unit) {\n        return val.convertTo(unit.value);\n    },\n    round: function (n, f) {\n        var fraction = typeof(f) === \"undefined\" ? 0 : f.value;\n        return _math(function(num) { return num.toFixed(fraction); }, null, n);\n    },\n    pi: function () {\n        return new(tree.Dimension)(Math.PI);\n    },\n    mod: function(a, b) {\n        return new(tree.Dimension)(a.value % b.value, a.unit);\n    },\n    pow: function(x, y) {\n        if (typeof x === \"number\" && typeof y === \"number\") {\n            x = new(tree.Dimension)(x);\n            y = new(tree.Dimension)(y);\n        } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) {\n            throw { type: \"Argument\", message: \"arguments must be numbers\" };\n        }\n\n        return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit);\n    },\n    _minmax: function (isMin, args) {\n        args = Array.prototype.slice.call(args);\n        switch(args.length) {\n            case 0: throw { type: \"Argument\", message: \"one or more arguments required\" };\n        }\n        var i, j, current, currentUnified, referenceUnified, unit, unitStatic, unitClone,\n            order  = [], // elems only contains original argument values.\n            values = {}; // key is the unit.toString() for unified tree.Dimension values,\n                         // value is the index into the order array.\n        for (i = 0; i < args.length; i++) {\n            current = args[i];\n            if (!(current instanceof tree.Dimension)) {\n                if(Array.isArray(args[i].value)) {\n                    Array.prototype.push.apply(args, Array.prototype.slice.call(args[i].value));\n                }\n                continue;\n            }\n            currentUnified = current.unit.toString() === \"\" && unitClone !== undefined ? new(tree.Dimension)(current.value, unitClone).unify() : current.unify();\n            unit = currentUnified.unit.toString() === \"\" && unitStatic !== undefined ? unitStatic : currentUnified.unit.toString();\t\t\t\n            unitStatic = unit !== \"\" && unitStatic === undefined || unit !== \"\" && order[0].unify().unit.toString() === \"\" ? unit : unitStatic;\n            unitClone = unit !== \"\" && unitClone === undefined ? current.unit.toString() : unitClone;\n            j = values[\"\"] !== undefined && unit !== \"\" && unit === unitStatic ? values[\"\"] : values[unit];\n            if (j === undefined) {\n                if(unitStatic !== undefined && unit !== unitStatic) {\n                    throw{ type: \"Argument\", message: \"incompatible types\" };\n                }\n                values[unit] = order.length;\n                order.push(current);\n                continue;\n            }\n            referenceUnified = order[j].unit.toString() === \"\" && unitClone !== undefined ? new(tree.Dimension)(order[j].value, unitClone).unify() : order[j].unify();\n            if ( isMin && currentUnified.value < referenceUnified.value ||\n                !isMin && currentUnified.value > referenceUnified.value) {\n                order[j] = current;\n            }\n        }\n        if (order.length == 1) {\n            return order[0];\n        }\n        args = order.map(function (a) { return a.toCSS(this.env); }).join(this.env.compress ? \",\" : \", \");\n        return new(tree.Anonymous)((isMin ? \"min\" : \"max\") + \"(\" + args + \")\");\n    },\n    min: function () {\n        return this._minmax(true, arguments);\n    },\n    max: function () {\n        return this._minmax(false, arguments);\n    },\n    \"get-unit\": function (n) {\n        return new(tree.Anonymous)(n.unit);\n    },\n    argb: function (color) {\n        return new(tree.Anonymous)(color.toARGB());\n    },\n    percentage: function (n) {\n        return new(tree.Dimension)(n.value * 100, '%');\n    },\n    color: function (n) {\n        if (n instanceof tree.Quoted) {\n            var colorCandidate = n.value,\n                returnColor;\n            returnColor = tree.Color.fromKeyword(colorCandidate);\n            if (returnColor) {\n                return returnColor;\n            }\n            if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/.test(colorCandidate)) {\n                return new(tree.Color)(colorCandidate.slice(1));\n            }\n            throw { type: \"Argument\", message: \"argument must be a color keyword or 3/6 digit hex e.g. #FFF\" };\n        } else {\n            throw { type: \"Argument\", message: \"argument must be a string\" };\n        }\n    },\n    iscolor: function (n) {\n        return this._isa(n, tree.Color);\n    },\n    isnumber: function (n) {\n        return this._isa(n, tree.Dimension);\n    },\n    isstring: function (n) {\n        return this._isa(n, tree.Quoted);\n    },\n    iskeyword: function (n) {\n        return this._isa(n, tree.Keyword);\n    },\n    isurl: function (n) {\n        return this._isa(n, tree.URL);\n    },\n    ispixel: function (n) {\n        return this.isunit(n, 'px');\n    },\n    ispercentage: function (n) {\n        return this.isunit(n, '%');\n    },\n    isem: function (n) {\n        return this.isunit(n, 'em');\n    },\n    isunit: function (n, unit) {\n        return (n instanceof tree.Dimension) && n.unit.is(unit.value || unit) ? tree.True : tree.False;\n    },\n    _isa: function (n, Type) {\n        return (n instanceof Type) ? tree.True : tree.False;\n    },\n    tint: function(color, amount) {\n        return this.mix(this.rgb(255,255,255), color, amount);\n    },\n    shade: function(color, amount) {\n        return this.mix(this.rgb(0, 0, 0), color, amount);\n    },   \n    extract: function(values, index) {\n        index = index.value - 1; // (1-based index)       \n        // handle non-array values as an array of length 1\n        // return 'undefined' if index is invalid\n        return Array.isArray(values.value) \n            ? values.value[index] : Array(values)[index];\n    },\n    length: function(values) {\n        var n = Array.isArray(values.value) ? values.value.length : 1;\n        return new tree.Dimension(n);\n    },\n\n    \"data-uri\": function(mimetypeNode, filePathNode) {\n\n        if (typeof window !== 'undefined') {\n            return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env);\n        }\n\n        var mimetype = mimetypeNode.value;\n        var filePath = (filePathNode && filePathNode.value);\n\n        var fs = require('fs'),\n            path = require('path'),\n            useBase64 = false;\n\n        if (arguments.length < 2) {\n            filePath = mimetype;\n        }\n\n        if (this.env.isPathRelative(filePath)) {\n            if (this.currentFileInfo.relativeUrls) {\n                filePath = path.join(this.currentFileInfo.currentDirectory, filePath);\n            } else {\n                filePath = path.join(this.currentFileInfo.entryPath, filePath);\n            }\n        }\n\n        // detect the mimetype if not given\n        if (arguments.length < 2) {\n            var mime;\n            try {\n                mime = require('mime');\n            } catch (ex) {\n                mime = tree._mime;\n            }\n\n            mimetype = mime.lookup(filePath);\n\n            // use base 64 unless it's an ASCII or UTF-8 format\n            var charset = mime.charsets.lookup(mimetype);\n            useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0;\n            if (useBase64) { mimetype += ';base64'; }\n        }\n        else {\n            useBase64 = /;base64$/.test(mimetype);\n        }\n\n        var buf = fs.readFileSync(filePath);\n\n        // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded\n        // and the --ieCompat flag is enabled, return a normal url() instead.\n        var DATA_URI_MAX_KB = 32,\n            fileSizeInKB = parseInt((buf.length / 1024), 10);\n        if (fileSizeInKB >= DATA_URI_MAX_KB) {\n\n            if (this.env.ieCompat !== false) {\n                if (!this.env.silent) {\n                    console.warn(\"Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!\", filePath, fileSizeInKB, DATA_URI_MAX_KB);\n                }\n\n                return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env);\n            }\n        }\n\n        buf = useBase64 ? buf.toString('base64')\n                        : encodeURIComponent(buf);\n\n        var uri = \"\\\"data:\" + mimetype + ',' + buf + \"\\\"\";\n        return new(tree.URL)(new(tree.Anonymous)(uri));\n    },\n\n    \"svg-gradient\": function(direction) {\n\n        function throwArgumentDescriptor() {\n            throw { type: \"Argument\", message: \"svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]\" };\n        }\n\n        if (arguments.length < 3) {\n            throwArgumentDescriptor();\n        }\n        var stops = Array.prototype.slice.call(arguments, 1),\n            gradientDirectionSvg,\n            gradientType = \"linear\",\n            rectangleDimension = 'x=\"0\" y=\"0\" width=\"1\" height=\"1\"',\n            useBase64 = true,\n            renderEnv = {compress: false},\n            returner,\n            directionValue = direction.toCSS(renderEnv),\n            i, color, position, positionValue, alpha;\n\n        switch (directionValue) {\n            case \"to bottom\":\n                gradientDirectionSvg = 'x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\"';\n                break;\n            case \"to right\":\n                gradientDirectionSvg = 'x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\"';\n                break;\n            case \"to bottom right\":\n                gradientDirectionSvg = 'x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"';\n                break;\n            case \"to top right\":\n                gradientDirectionSvg = 'x1=\"0%\" y1=\"100%\" x2=\"100%\" y2=\"0%\"';\n                break;\n            case \"ellipse\":\n            case \"ellipse at center\":\n                gradientType = \"radial\";\n                gradientDirectionSvg = 'cx=\"50%\" cy=\"50%\" r=\"75%\"';\n                rectangleDimension = 'x=\"-50\" y=\"-50\" width=\"101\" height=\"101\"';\n                break;\n            default:\n                throw { type: \"Argument\", message: \"svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'\" };\n        }\n        returner = '<?xml version=\"1.0\" ?>' +\n            '<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"100%\" height=\"100%\" viewBox=\"0 0 1 1\" preserveAspectRatio=\"none\">' +\n            '<' + gradientType + 'Gradient id=\"gradient\" gradientUnits=\"userSpaceOnUse\" ' + gradientDirectionSvg + '>';\n\n        for (i = 0; i < stops.length; i+= 1) {\n            if (stops[i].value) {\n                color = stops[i].value[0];\n                position = stops[i].value[1];\n            } else {\n                color = stops[i];\n                position = undefined;\n            }\n\n            if (!(color instanceof tree.Color) || (!((i === 0 || i+1 === stops.length) && position === undefined) && !(position instanceof tree.Dimension))) {\n                throwArgumentDescriptor();\n            }\n            positionValue = position ? position.toCSS(renderEnv) : i === 0 ? \"0%\" : \"100%\";\n            alpha = color.alpha;\n            returner += '<stop offset=\"' + positionValue + '\" stop-color=\"' + color.toRGB() + '\"' + (alpha < 1 ? ' stop-opacity=\"' + alpha + '\"' : '') + '/>';\n        }\n        returner += '</' + gradientType + 'Gradient>' +\n                    '<rect ' + rectangleDimension + ' fill=\"url(#gradient)\" /></svg>';\n\n        if (useBase64) {\n            try {\n                returner = require('./encoder').encodeBase64(returner); // TODO browser implementation\n            } catch(e) {\n                useBase64 = false;\n            }\n        }\n\n        returner = \"'data:image/svg+xml\" + (useBase64 ? \";base64\" : \"\") + \",\" + returner + \"'\";\n        return new(tree.URL)(new(tree.Anonymous)(returner));\n    }\n};\n\n// these static methods are used as a fallback when the optional 'mime' dependency is missing\ntree._mime = {\n    // this map is intentionally incomplete\n    // if you want more, install 'mime' dep\n    _types: {\n        '.htm' : 'text/html',\n        '.html': 'text/html',\n        '.gif' : 'image/gif',\n        '.jpg' : 'image/jpeg',\n        '.jpeg': 'image/jpeg',\n        '.png' : 'image/png'\n    },\n    lookup: function (filepath) {\n        var ext = require('path').extname(filepath),\n            type = tree._mime._types[ext];\n        if (type === undefined) {\n            throw new Error('Optional dependency \"mime\" is required for ' + ext);\n        }\n        return type;\n    },\n    charsets: {\n        lookup: function (type) {\n            // assumes all text types are UTF-8\n            return type && (/^text\\//).test(type) ? 'UTF-8' : '';\n        }\n    }\n};\n\n// Math\n\nvar mathFunctions = {\n // name,  unit\n    ceil:  null, \n    floor: null, \n    sqrt:  null, \n    abs:   null,\n    tan:   \"\", \n    sin:   \"\", \n    cos:   \"\",\n    atan:  \"rad\", \n    asin:  \"rad\", \n    acos:  \"rad\"\n};\n\nfunction _math(fn, unit, n) {\n    if (!(n instanceof tree.Dimension)) {\n        throw { type: \"Argument\", message: \"argument must be a number\" };\n    }\n    if (unit == null) {\n        unit = n.unit;\n    } else {\n        n = n.unify();\n    }\n    return new(tree.Dimension)(fn(parseFloat(n.value)), unit);\n}\n\n// ~ End of Math\n\n// Color Blending\n// ref: http://www.w3.org/TR/compositing-1\n\nfunction colorBlend(mode, color1, color2) {\n    var ab = color1.alpha, cb, // backdrop\n        as = color2.alpha, cs, // source\n        ar, cr, r = [];        // result\n        \n    ar = as + ab * (1 - as);\n    for (var i = 0; i < 3; i++) {\n        cb = color1.rgb[i] / 255;\n        cs = color2.rgb[i] / 255;\n        cr = mode(cb, cs);\n        if (ar) {\n            cr = (as * cs + ab * (cb \n                - as * (cb + cs - cr))) / ar;\n        }\n        r[i] = cr * 255;\n    }\n    \n    return new(tree.Color)(r, ar);\n}\n\nvar colorBlendMode = {\n    multiply: function(cb, cs) {\n        return cb * cs;\n    },\n    screen: function(cb, cs) {\n        return cb + cs - cb * cs;\n    },   \n    overlay: function(cb, cs) {\n        cb *= 2;\n        return (cb <= 1)\n            ? colorBlendMode.multiply(cb, cs)\n            : colorBlendMode.screen(cb - 1, cs);\n    },\n    softlight: function(cb, cs) {\n        var d = 1, e = cb;\n        if (cs > 0.5) {\n            e = 1;\n            d = (cb > 0.25) ? Math.sqrt(cb)\n                : ((16 * cb - 12) * cb + 4) * cb;\n        }            \n        return cb - (1 - 2 * cs) * e * (d - cb);\n    },\n    hardlight: function(cb, cs) {\n        return colorBlendMode.overlay(cs, cb);\n    },\n    difference: function(cb, cs) {\n        return Math.abs(cb - cs);\n    },\n    exclusion: function(cb, cs) {\n        return cb + cs - 2 * cb * cs;\n    },\n\n    // non-w3c functions:\n    average: function(cb, cs) {\n        return (cb + cs) / 2;\n    },\n    negation: function(cb, cs) {\n        return 1 - Math.abs(cb + cs - 1);\n    }\n};\n\n// ~ End of Color Blending\n\ntree.defaultFunc = {\n    eval: function () {\n        var v = this.value_, e = this.error_;\n        if (e) {\n            throw e;\n        }\n        if (v != null) {\n            return v ? tree.True : tree.False;\n        }\n    },\n    value: function (v) {\n        this.value_ = v;\n    },\n    error: function (e) {\n        this.error_ = e;\n    },\n    reset: function () {\n        this.value_ = this.error_ = null;\n    }\n};\n\nfunction initFunctions() {\n    var f, tf = tree.functions;\n    \n    // math\n    for (f in mathFunctions) {\n        if (mathFunctions.hasOwnProperty(f)) {\n            tf[f] = _math.bind(null, Math[f], mathFunctions[f]);\n        }\n    }\n    \n    // color blending\n    for (f in colorBlendMode) {\n        if (colorBlendMode.hasOwnProperty(f)) {\n            tf[f] = colorBlend.bind(null, colorBlendMode[f]);\n        }\n    }\n    \n    // default\n    f = tree.defaultFunc;\n    tf[\"default\"] = f.eval.bind(f);\n    \n} initFunctions();\n\nfunction hsla(color) {\n    return tree.functions.hsla(color.h, color.s, color.l, color.a);\n}\n\nfunction scaled(n, size) {\n    if (n instanceof tree.Dimension && n.unit.is('%')) {\n        return parseFloat(n.value * size / 100);\n    } else {\n        return number(n);\n    }\n}\n\nfunction number(n) {\n    if (n instanceof tree.Dimension) {\n        return parseFloat(n.unit.is('%') ? n.value / 100 : n.value);\n    } else if (typeof(n) === 'number') {\n        return n;\n    } else {\n        throw {\n            error: \"RuntimeError\",\n            message: \"color functions take numbers as parameters\"\n        };\n    }\n}\n\nfunction clamp(val) {\n    return Math.min(1, Math.max(0, val));\n}\n\ntree.fround = function(env, value) {\n    var p;\n    if (env && (env.numPrecision != null)) {\n        p = Math.pow(10, env.numPrecision);\n        return Math.round(value * p) / p;\n    } else {\n        return value;\n    }\n};\n\ntree.functionCall = function(env, currentFileInfo) {\n    this.env = env;\n    this.currentFileInfo = currentFileInfo;\n};\n\ntree.functionCall.prototype = tree.functions;\n\n})(require('./tree'));\n\n(function (tree) {\n    tree.colors = {\n        'aliceblue':'#f0f8ff',\n        'antiquewhite':'#faebd7',\n        'aqua':'#00ffff',\n        'aquamarine':'#7fffd4',\n        'azure':'#f0ffff',\n        'beige':'#f5f5dc',\n        'bisque':'#ffe4c4',\n        'black':'#000000',\n        'blanchedalmond':'#ffebcd',\n        'blue':'#0000ff',\n        'blueviolet':'#8a2be2',\n        'brown':'#a52a2a',\n        'burlywood':'#deb887',\n        'cadetblue':'#5f9ea0',\n        'chartreuse':'#7fff00',\n        'chocolate':'#d2691e',\n        'coral':'#ff7f50',\n        'cornflowerblue':'#6495ed',\n        'cornsilk':'#fff8dc',\n        'crimson':'#dc143c',\n        'cyan':'#00ffff',\n        'darkblue':'#00008b',\n        'darkcyan':'#008b8b',\n        'darkgoldenrod':'#b8860b',\n        'darkgray':'#a9a9a9',\n        'darkgrey':'#a9a9a9',\n        'darkgreen':'#006400',\n        'darkkhaki':'#bdb76b',\n        'darkmagenta':'#8b008b',\n        'darkolivegreen':'#556b2f',\n        'darkorange':'#ff8c00',\n        'darkorchid':'#9932cc',\n        'darkred':'#8b0000',\n        'darksalmon':'#e9967a',\n        'darkseagreen':'#8fbc8f',\n        'darkslateblue':'#483d8b',\n        'darkslategray':'#2f4f4f',\n        'darkslategrey':'#2f4f4f',\n        'darkturquoise':'#00ced1',\n        'darkviolet':'#9400d3',\n        'deeppink':'#ff1493',\n        'deepskyblue':'#00bfff',\n        'dimgray':'#696969',\n        'dimgrey':'#696969',\n        'dodgerblue':'#1e90ff',\n        'firebrick':'#b22222',\n        'floralwhite':'#fffaf0',\n        'forestgreen':'#228b22',\n        'fuchsia':'#ff00ff',\n        'gainsboro':'#dcdcdc',\n        'ghostwhite':'#f8f8ff',\n        'gold':'#ffd700',\n        'goldenrod':'#daa520',\n        'gray':'#808080',\n        'grey':'#808080',\n        'green':'#008000',\n        'greenyellow':'#adff2f',\n        'honeydew':'#f0fff0',\n        'hotpink':'#ff69b4',\n        'indianred':'#cd5c5c',\n        'indigo':'#4b0082',\n        'ivory':'#fffff0',\n        'khaki':'#f0e68c',\n        'lavender':'#e6e6fa',\n        'lavenderblush':'#fff0f5',\n        'lawngreen':'#7cfc00',\n        'lemonchiffon':'#fffacd',\n        'lightblue':'#add8e6',\n        'lightcoral':'#f08080',\n        'lightcyan':'#e0ffff',\n        'lightgoldenrodyellow':'#fafad2',\n        'lightgray':'#d3d3d3',\n        'lightgrey':'#d3d3d3',\n        'lightgreen':'#90ee90',\n        'lightpink':'#ffb6c1',\n        'lightsalmon':'#ffa07a',\n        'lightseagreen':'#20b2aa',\n        'lightskyblue':'#87cefa',\n        'lightslategray':'#778899',\n        'lightslategrey':'#778899',\n        'lightsteelblue':'#b0c4de',\n        'lightyellow':'#ffffe0',\n        'lime':'#00ff00',\n        'limegreen':'#32cd32',\n        'linen':'#faf0e6',\n        'magenta':'#ff00ff',\n        'maroon':'#800000',\n        'mediumaquamarine':'#66cdaa',\n        'mediumblue':'#0000cd',\n        'mediumorchid':'#ba55d3',\n        'mediumpurple':'#9370d8',\n        'mediumseagreen':'#3cb371',\n        'mediumslateblue':'#7b68ee',\n        'mediumspringgreen':'#00fa9a',\n        'mediumturquoise':'#48d1cc',\n        'mediumvioletred':'#c71585',\n        'midnightblue':'#191970',\n        'mintcream':'#f5fffa',\n        'mistyrose':'#ffe4e1',\n        'moccasin':'#ffe4b5',\n        'navajowhite':'#ffdead',\n        'navy':'#000080',\n        'oldlace':'#fdf5e6',\n        'olive':'#808000',\n        'olivedrab':'#6b8e23',\n        'orange':'#ffa500',\n        'orangered':'#ff4500',\n        'orchid':'#da70d6',\n        'palegoldenrod':'#eee8aa',\n        'palegreen':'#98fb98',\n        'paleturquoise':'#afeeee',\n        'palevioletred':'#d87093',\n        'papayawhip':'#ffefd5',\n        'peachpuff':'#ffdab9',\n        'peru':'#cd853f',\n        'pink':'#ffc0cb',\n        'plum':'#dda0dd',\n        'powderblue':'#b0e0e6',\n        'purple':'#800080',\n        'red':'#ff0000',\n        'rosybrown':'#bc8f8f',\n        'royalblue':'#4169e1',\n        'saddlebrown':'#8b4513',\n        'salmon':'#fa8072',\n        'sandybrown':'#f4a460',\n        'seagreen':'#2e8b57',\n        'seashell':'#fff5ee',\n        'sienna':'#a0522d',\n        'silver':'#c0c0c0',\n        'skyblue':'#87ceeb',\n        'slateblue':'#6a5acd',\n        'slategray':'#708090',\n        'slategrey':'#708090',\n        'snow':'#fffafa',\n        'springgreen':'#00ff7f',\n        'steelblue':'#4682b4',\n        'tan':'#d2b48c',\n        'teal':'#008080',\n        'thistle':'#d8bfd8',\n        'tomato':'#ff6347',\n        'turquoise':'#40e0d0',\n        'violet':'#ee82ee',\n        'wheat':'#f5deb3',\n        'white':'#ffffff',\n        'whitesmoke':'#f5f5f5',\n        'yellow':'#ffff00',\n        'yellowgreen':'#9acd32'\n    };\n})(require('./tree'));\n\n(function (tree) {\n\ntree.debugInfo = function(env, ctx, lineSeperator) {\n    var result=\"\";\n    if (env.dumpLineNumbers && !env.compress) {\n        switch(env.dumpLineNumbers) {\n            case 'comments':\n                result = tree.debugInfo.asComment(ctx);\n                break;\n            case 'mediaquery':\n                result = tree.debugInfo.asMediaQuery(ctx);\n                break;\n            case 'all':\n                result = tree.debugInfo.asComment(ctx) + (lineSeperator || \"\") + tree.debugInfo.asMediaQuery(ctx);\n                break;\n        }\n    }\n    return result;\n};\n\ntree.debugInfo.asComment = function(ctx) {\n    return '/* line ' + ctx.debugInfo.lineNumber + ', ' + ctx.debugInfo.fileName + ' */\\n';\n};\n\ntree.debugInfo.asMediaQuery = function(ctx) {\n    return '@media -sass-debug-info{filename{font-family:' +\n        ('file://' + ctx.debugInfo.fileName).replace(/([.:\\/\\\\])/g, function (a) {\n            if (a == '\\\\') {\n                a = '\\/';\n            }\n            return '\\\\' + a;\n        }) +\n        '}line{font-family:\\\\00003' + ctx.debugInfo.lineNumber + '}}\\n';\n};\n\ntree.find = function (obj, fun) {\n    for (var i = 0, r; i < obj.length; i++) {\n        r = fun.call(obj, obj[i]);\n        if (r) { return r; }\n    }\n    return null;\n};\n\ntree.jsify = function (obj) {\n    if (Array.isArray(obj.value) && (obj.value.length > 1)) {\n        return '[' + obj.value.map(function (v) { return v.toCSS(false); }).join(', ') + ']';\n    } else {\n        return obj.toCSS(false);\n    }\n};\n\ntree.toCSS = function (env) {\n    var strs = [];\n    this.genCSS(env, {\n        add: function(chunk, fileInfo, index) {\n            strs.push(chunk);\n        },\n        isEmpty: function () {\n            return strs.length === 0;\n        }\n    });\n    return strs.join('');\n};\n\ntree.outputRuleset = function (env, output, rules) {\n    var ruleCnt = rules.length, i;\n    env.tabLevel = (env.tabLevel | 0) + 1;\n\n    // Compressed\n    if (env.compress) {\n        output.add('{');\n        for (i = 0; i < ruleCnt; i++) {\n            rules[i].genCSS(env, output);\n        }\n        output.add('}');\n        env.tabLevel--;\n        return;\n    }\n\n    // Non-compressed\n    var tabSetStr = '\\n' + Array(env.tabLevel).join(\"  \"), tabRuleStr = tabSetStr + \"  \";\n    if (!ruleCnt) {\n        output.add(\" {\" + tabSetStr + '}');\n    } else {\n        output.add(\" {\" + tabRuleStr);\n        rules[0].genCSS(env, output);\n        for (i = 1; i < ruleCnt; i++) {\n            output.add(tabRuleStr);\n            rules[i].genCSS(env, output);\n        }\n        output.add(tabSetStr + '}');\n    }\n\n    env.tabLevel--;\n};\n\n})(require('./tree'));\n\n(function (tree) {\n\ntree.Alpha = function (val) {\n    this.value = val;\n};\ntree.Alpha.prototype = {\n    type: \"Alpha\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        if (this.value.eval) { return new tree.Alpha(this.value.eval(env)); }\n        return this;\n    },\n    genCSS: function (env, output) {\n        output.add(\"alpha(opacity=\");\n\n        if (this.value.genCSS) {\n            this.value.genCSS(env, output);\n        } else {\n            output.add(this.value);\n        }\n\n        output.add(\")\");\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Anonymous = function (string, index, currentFileInfo, mapLines) {\n    this.value = string.value || string;\n    this.index = index;\n    this.mapLines = mapLines;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Anonymous.prototype = {\n    type: \"Anonymous\",\n    eval: function () { \n        return new tree.Anonymous(this.value, this.index, this.currentFileInfo, this.mapLines);\n    },\n    compare: function (x) {\n        if (!x.toCSS) {\n            return -1;\n        }\n        \n        var left = this.toCSS(),\n            right = x.toCSS();\n        \n        if (left === right) {\n            return 0;\n        }\n        \n        return left < right ? -1 : 1;\n    },\n    genCSS: function (env, output) {\n        output.add(this.value, this.currentFileInfo, this.index, this.mapLines);\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Assignment = function (key, val) {\n    this.key = key;\n    this.value = val;\n};\ntree.Assignment.prototype = {\n    type: \"Assignment\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    eval: function (env) {\n        if (this.value.eval) {\n            return new(tree.Assignment)(this.key, this.value.eval(env));\n        }\n        return this;\n    },\n    genCSS: function (env, output) {\n        output.add(this.key + '=');\n        if (this.value.genCSS) {\n            this.value.genCSS(env, output);\n        } else {\n            output.add(this.value);\n        }\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\n//\n// A function call node.\n//\ntree.Call = function (name, args, index, currentFileInfo) {\n    this.name = name;\n    this.args = args;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Call.prototype = {\n    type: \"Call\",\n    accept: function (visitor) {\n        if (this.args) {\n            this.args = visitor.visitArray(this.args);\n        }\n    },\n    //\n    // When evaluating a function call,\n    // we either find the function in `tree.functions` [1],\n    // in which case we call it, passing the  evaluated arguments,\n    // if this returns null or we cannot find the function, we \n    // simply print it out as it appeared originally [2].\n    //\n    // The *functions.js* file contains the built-in functions.\n    //\n    // The reason why we evaluate the arguments, is in the case where\n    // we try to pass a variable to a function, like: `saturate(@color)`.\n    // The function should receive the value, not the variable.\n    //\n    eval: function (env) {\n        var args = this.args.map(function (a) { return a.eval(env); }),\n            nameLC = this.name.toLowerCase(),\n            result, func;\n\n        if (nameLC in tree.functions) { // 1.\n            try {\n                func = new tree.functionCall(env, this.currentFileInfo);\n                result = func[nameLC].apply(func, args);\n                if (result != null) {\n                    return result;\n                }\n            } catch (e) {\n                throw { type: e.type || \"Runtime\",\n                        message: \"error evaluating function `\" + this.name + \"`\" +\n                                 (e.message ? ': ' + e.message : ''),\n                        index: this.index, filename: this.currentFileInfo.filename };\n            }\n        }\n\n        return new tree.Call(this.name, args, this.index, this.currentFileInfo);\n    },\n\n    genCSS: function (env, output) {\n        output.add(this.name + \"(\", this.currentFileInfo, this.index);\n\n        for(var i = 0; i < this.args.length; i++) {\n            this.args[i].genCSS(env, output);\n            if (i + 1 < this.args.length) {\n                output.add(\", \");\n            }\n        }\n\n        output.add(\")\");\n    },\n\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n//\n// RGB Colors - #ff0014, #eee\n//\ntree.Color = function (rgb, a) {\n    //\n    // The end goal here, is to parse the arguments\n    // into an integer triplet, such as `128, 255, 0`\n    //\n    // This facilitates operations and conversions.\n    //\n    if (Array.isArray(rgb)) {\n        this.rgb = rgb;\n    } else if (rgb.length == 6) {\n        this.rgb = rgb.match(/.{2}/g).map(function (c) {\n            return parseInt(c, 16);\n        });\n    } else {\n        this.rgb = rgb.split('').map(function (c) {\n            return parseInt(c + c, 16);\n        });\n    }\n    this.alpha = typeof(a) === 'number' ? a : 1;\n};\n\nvar transparentKeyword = \"transparent\";\n\ntree.Color.prototype = {\n    type: \"Color\",\n    eval: function () { return this; },\n    luma: function () {\n        var r = this.rgb[0] / 255,\n            g = this.rgb[1] / 255,\n            b = this.rgb[2] / 255;\n\n        r = (r <= 0.03928) ? r / 12.92 : Math.pow(((r + 0.055) / 1.055), 2.4);\n        g = (g <= 0.03928) ? g / 12.92 : Math.pow(((g + 0.055) / 1.055), 2.4);\n        b = (b <= 0.03928) ? b / 12.92 : Math.pow(((b + 0.055) / 1.055), 2.4);\n\n        return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n    },\n\n    genCSS: function (env, output) {\n        output.add(this.toCSS(env));\n    },\n    toCSS: function (env, doNotCompress) {\n        var compress = env && env.compress && !doNotCompress,\n            alpha = tree.fround(env, this.alpha);\n\n        // If we have some transparency, the only way to represent it\n        // is via `rgba`. Otherwise, we use the hex representation,\n        // which has better compatibility with older browsers.\n        // Values are capped between `0` and `255`, rounded and zero-padded.\n        if (alpha < 1) {\n            if (alpha === 0 && this.isTransparentKeyword) {\n                return transparentKeyword;\n            }\n            return \"rgba(\" + this.rgb.map(function (c) {\n                return clamp(Math.round(c), 255);\n            }).concat(clamp(alpha, 1))\n                .join(',' + (compress ? '' : ' ')) + \")\";\n        } else {\n            var color = this.toRGB();\n\n            if (compress) {\n                var splitcolor = color.split('');\n\n                // Convert color to short format\n                if (splitcolor[1] === splitcolor[2] && splitcolor[3] === splitcolor[4] && splitcolor[5] === splitcolor[6]) {\n                    color = '#' + splitcolor[1] + splitcolor[3] + splitcolor[5];\n                }\n            }\n\n            return color;\n        }\n    },\n\n    //\n    // Operations have to be done per-channel, if not,\n    // channels will spill onto each other. Once we have\n    // our result, in the form of an integer triplet,\n    // we create a new Color node to hold the result.\n    //\n    operate: function (env, op, other) {\n        var rgb = [];\n        var alpha = this.alpha * (1 - other.alpha) + other.alpha;\n        for (var c = 0; c < 3; c++) {\n            rgb[c] = tree.operate(env, op, this.rgb[c], other.rgb[c]);\n        }\n        return new(tree.Color)(rgb, alpha);\n    },\n\n    toRGB: function () {\n        return toHex(this.rgb);\n    },\n\n    toHSL: function () {\n        var r = this.rgb[0] / 255,\n            g = this.rgb[1] / 255,\n            b = this.rgb[2] / 255,\n            a = this.alpha;\n\n        var max = Math.max(r, g, b), min = Math.min(r, g, b);\n        var h, s, l = (max + min) / 2, d = max - min;\n\n        if (max === min) {\n            h = s = 0;\n        } else {\n            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n\n            switch (max) {\n                case r: h = (g - b) / d + (g < b ? 6 : 0); break;\n                case g: h = (b - r) / d + 2;               break;\n                case b: h = (r - g) / d + 4;               break;\n            }\n            h /= 6;\n        }\n        return { h: h * 360, s: s, l: l, a: a };\n    },\n    //Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript\n    toHSV: function () {\n        var r = this.rgb[0] / 255,\n            g = this.rgb[1] / 255,\n            b = this.rgb[2] / 255,\n            a = this.alpha;\n\n        var max = Math.max(r, g, b), min = Math.min(r, g, b);\n        var h, s, v = max;\n\n        var d = max - min;\n        if (max === 0) {\n            s = 0;\n        } else {\n            s = d / max;\n        }\n\n        if (max === min) {\n            h = 0;\n        } else {\n            switch(max){\n                case r: h = (g - b) / d + (g < b ? 6 : 0); break;\n                case g: h = (b - r) / d + 2; break;\n                case b: h = (r - g) / d + 4; break;\n            }\n            h /= 6;\n        }\n        return { h: h * 360, s: s, v: v, a: a };\n    },\n    toARGB: function () {\n        return toHex([this.alpha * 255].concat(this.rgb));\n    },\n    compare: function (x) {\n        if (!x.rgb) {\n            return -1;\n        }\n        \n        return (x.rgb[0] === this.rgb[0] &&\n            x.rgb[1] === this.rgb[1] &&\n            x.rgb[2] === this.rgb[2] &&\n            x.alpha === this.alpha) ? 0 : -1;\n    }\n};\n\ntree.Color.fromKeyword = function(keyword) {\n    keyword = keyword.toLowerCase();\n\n    if (tree.colors.hasOwnProperty(keyword)) {\n        // detect named color\n        return new(tree.Color)(tree.colors[keyword].slice(1));\n    }\n    if (keyword === transparentKeyword) {\n        var transparent = new(tree.Color)([0, 0, 0], 0);\n        transparent.isTransparentKeyword = true;\n        return transparent;\n    }\n};\n\nfunction toHex(v) {\n    return '#' + v.map(function (c) {\n        c = clamp(Math.round(c), 255);\n        return (c < 16 ? '0' : '') + c.toString(16);\n    }).join('');\n}\n\nfunction clamp(v, max) {\n    return Math.min(Math.max(v, 0), max); \n}\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Comment = function (value, silent, index, currentFileInfo) {\n    this.value = value;\n    this.silent = !!silent;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Comment.prototype = {\n    type: \"Comment\",\n    genCSS: function (env, output) {\n        if (this.debugInfo) {\n            output.add(tree.debugInfo(env, this), this.currentFileInfo, this.index);\n        }\n        output.add(this.value.trim()); //TODO shouldn't need to trim, we shouldn't grab the \\n\n    },\n    toCSS: tree.toCSS,\n    isSilent: function(env) {\n        var isReference = (this.currentFileInfo && this.currentFileInfo.reference && !this.isReferenced),\n            isCompressed = env.compress && !this.value.match(/^\\/\\*!/);\n        return this.silent || isReference || isCompressed;\n    },\n    eval: function () { return this; },\n    markReferenced: function () {\n        this.isReferenced = true;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Condition = function (op, l, r, i, negate) {\n    this.op = op.trim();\n    this.lvalue = l;\n    this.rvalue = r;\n    this.index = i;\n    this.negate = negate;\n};\ntree.Condition.prototype = {\n    type: \"Condition\",\n    accept: function (visitor) {\n        this.lvalue = visitor.visit(this.lvalue);\n        this.rvalue = visitor.visit(this.rvalue);\n    },\n    eval: function (env) {\n        var a = this.lvalue.eval(env),\n            b = this.rvalue.eval(env);\n\n        var i = this.index, result;\n\n        result = (function (op) {\n            switch (op) {\n                case 'and':\n                    return a && b;\n                case 'or':\n                    return a || b;\n                default:\n                    if (a.compare) {\n                        result = a.compare(b);\n                    } else if (b.compare) {\n                        result = b.compare(a);\n                    } else {\n                        throw { type: \"Type\",\n                                message: \"Unable to perform comparison\",\n                                index: i };\n                    }\n                    switch (result) {\n                        case -1: return op === '<' || op === '=<' || op === '<=';\n                        case  0: return op === '=' || op === '>=' || op === '=<' || op === '<=';\n                        case  1: return op === '>' || op === '>=';\n                    }\n            }\n        })(this.op);\n        return this.negate ? !result : result;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.DetachedRuleset = function (ruleset, frames) {\n    this.ruleset = ruleset;\n    this.frames = frames;\n};\ntree.DetachedRuleset.prototype = {\n    type: \"DetachedRuleset\",\n    accept: function (visitor) {\n        this.ruleset = visitor.visit(this.ruleset);\n    },\n    eval: function (env) {\n        var frames = this.frames || env.frames.slice(0);\n        return new tree.DetachedRuleset(this.ruleset, frames);\n    },\n    callEval: function (env) {\n        return this.ruleset.eval(this.frames ? new(tree.evalEnv)(env, this.frames.concat(env.frames)) : env);\n    }\n};\n})(require('../tree'));\n\n(function (tree) {\n\n//\n// A number with a unit\n//\ntree.Dimension = function (value, unit) {\n    this.value = parseFloat(value);\n    this.unit = (unit && unit instanceof tree.Unit) ? unit :\n      new(tree.Unit)(unit ? [unit] : undefined);\n};\n\ntree.Dimension.prototype = {\n    type: \"Dimension\",\n    accept: function (visitor) {\n        this.unit = visitor.visit(this.unit);\n    },\n    eval: function (env) {\n        return this;\n    },\n    toColor: function () {\n        return new(tree.Color)([this.value, this.value, this.value]);\n    },\n    genCSS: function (env, output) {\n        if ((env && env.strictUnits) && !this.unit.isSingular()) {\n            throw new Error(\"Multiple units in dimension. Correct the units or use the unit function. Bad unit: \"+this.unit.toString());\n        }\n\n        var value = tree.fround(env, this.value),\n            strValue = String(value);\n\n        if (value !== 0 && value < 0.000001 && value > -0.000001) {\n            // would be output 1e-6 etc.\n            strValue = value.toFixed(20).replace(/0+$/, \"\");\n        }\n\n        if (env && env.compress) {\n            // Zero values doesn't need a unit\n            if (value === 0 && this.unit.isLength()) {\n                output.add(strValue);\n                return;\n            }\n\n            // Float values doesn't need a leading zero\n            if (value > 0 && value < 1) {\n                strValue = (strValue).substr(1);\n            }\n        }\n\n        output.add(strValue);\n        this.unit.genCSS(env, output);\n    },\n    toCSS: tree.toCSS,\n\n    // In an operation between two Dimensions,\n    // we default to the first Dimension's unit,\n    // so `1px + 2` will yield `3px`.\n    operate: function (env, op, other) {\n        /*jshint noempty:false */\n        var value = tree.operate(env, op, this.value, other.value),\n            unit = this.unit.clone();\n\n        if (op === '+' || op === '-') {\n            if (unit.numerator.length === 0 && unit.denominator.length === 0) {\n                unit.numerator = other.unit.numerator.slice(0);\n                unit.denominator = other.unit.denominator.slice(0);\n            } else if (other.unit.numerator.length === 0 && unit.denominator.length === 0) {\n                // do nothing\n            } else {\n                other = other.convertTo(this.unit.usedUnits());\n\n                if(env.strictUnits && other.unit.toString() !== unit.toString()) {\n                  throw new Error(\"Incompatible units. Change the units or use the unit function. Bad units: '\" + unit.toString() +\n                    \"' and '\" + other.unit.toString() + \"'.\");\n                }\n\n                value = tree.operate(env, op, this.value, other.value);\n            }\n        } else if (op === '*') {\n            unit.numerator = unit.numerator.concat(other.unit.numerator).sort();\n            unit.denominator = unit.denominator.concat(other.unit.denominator).sort();\n            unit.cancel();\n        } else if (op === '/') {\n            unit.numerator = unit.numerator.concat(other.unit.denominator).sort();\n            unit.denominator = unit.denominator.concat(other.unit.numerator).sort();\n            unit.cancel();\n        }\n        return new(tree.Dimension)(value, unit);\n    },\n\n    compare: function (other) {\n        if (other instanceof tree.Dimension) {\n            var a, b,\n                aValue, bValue;\n            \n            if (this.unit.isEmpty() || other.unit.isEmpty()) {\n                a = this;\n                b = other;\n            } else {\n                a = this.unify();\n                b = other.unify();\n                if (a.unit.compare(b.unit) !== 0) {\n                    return -1;\n                }                \n            }\n            aValue = a.value;\n            bValue = b.value;\n\n            if (bValue > aValue) {\n                return -1;\n            } else if (bValue < aValue) {\n                return 1;\n            } else {\n                return 0;\n            }\n        } else {\n            return -1;\n        }\n    },\n\n    unify: function () {\n        return this.convertTo({ length: 'px', duration: 's', angle: 'rad' });\n    },\n\n    convertTo: function (conversions) {\n        var value = this.value, unit = this.unit.clone(),\n            i, groupName, group, targetUnit, derivedConversions = {}, applyUnit;\n\n        if (typeof conversions === 'string') {\n            for(i in tree.UnitConversions) {\n                if (tree.UnitConversions[i].hasOwnProperty(conversions)) {\n                    derivedConversions = {};\n                    derivedConversions[i] = conversions;\n                }\n            }\n            conversions = derivedConversions;\n        }\n        applyUnit = function (atomicUnit, denominator) {\n          /*jshint loopfunc:true */\n            if (group.hasOwnProperty(atomicUnit)) {\n                if (denominator) {\n                    value = value / (group[atomicUnit] / group[targetUnit]);\n                } else {\n                    value = value * (group[atomicUnit] / group[targetUnit]);\n                }\n\n                return targetUnit;\n            }\n\n            return atomicUnit;\n        };\n\n        for (groupName in conversions) {\n            if (conversions.hasOwnProperty(groupName)) {\n                targetUnit = conversions[groupName];\n                group = tree.UnitConversions[groupName];\n\n                unit.map(applyUnit);\n            }\n        }\n\n        unit.cancel();\n\n        return new(tree.Dimension)(value, unit);\n    }\n};\n\n// http://www.w3.org/TR/css3-values/#absolute-lengths\ntree.UnitConversions = {\n    length: {\n         'm': 1,\n        'cm': 0.01,\n        'mm': 0.001,\n        'in': 0.0254,\n        'px': 0.0254 / 96,\n        'pt': 0.0254 / 72,\n        'pc': 0.0254 / 72 * 12\n    },\n    duration: {\n        's': 1,\n        'ms': 0.001\n    },\n    angle: {\n        'rad': 1/(2*Math.PI),\n        'deg': 1/360,\n        'grad': 1/400,\n        'turn': 1\n    }\n};\n\ntree.Unit = function (numerator, denominator, backupUnit) {\n    this.numerator = numerator ? numerator.slice(0).sort() : [];\n    this.denominator = denominator ? denominator.slice(0).sort() : [];\n    this.backupUnit = backupUnit;\n};\n\ntree.Unit.prototype = {\n    type: \"Unit\",\n    clone: function () {\n        return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0), this.backupUnit);\n    },\n    genCSS: function (env, output) {\n        if (this.numerator.length >= 1) {\n            output.add(this.numerator[0]);\n        } else\n        if (this.denominator.length >= 1) {\n            output.add(this.denominator[0]);\n        } else\n        if ((!env || !env.strictUnits) && this.backupUnit) {\n            output.add(this.backupUnit);\n        }\n    },\n    toCSS: tree.toCSS,\n\n    toString: function () {\n      var i, returnStr = this.numerator.join(\"*\");\n      for (i = 0; i < this.denominator.length; i++) {\n          returnStr += \"/\" + this.denominator[i];\n      }\n      return returnStr;\n    },\n\n    compare: function (other) {\n        return this.is(other.toString()) ? 0 : -1;\n    },\n\n    is: function (unitString) {\n        return this.toString() === unitString;\n    },\n\n    isLength: function () {\n        return Boolean(this.toCSS().match(/px|em|%|in|cm|mm|pc|pt|ex/));\n    },\n\n    isEmpty: function () {\n        return this.numerator.length === 0 && this.denominator.length === 0;\n    },\n\n    isSingular: function() {\n        return this.numerator.length <= 1 && this.denominator.length === 0;\n    },\n\n    map: function(callback) {\n        var i;\n\n        for (i = 0; i < this.numerator.length; i++) {\n            this.numerator[i] = callback(this.numerator[i], false);\n        }\n\n        for (i = 0; i < this.denominator.length; i++) {\n            this.denominator[i] = callback(this.denominator[i], true);\n        }\n    },\n\n    usedUnits: function() {\n        var group, result = {}, mapUnit;\n\n        mapUnit = function (atomicUnit) {\n        /*jshint loopfunc:true */\n            if (group.hasOwnProperty(atomicUnit) && !result[groupName]) {\n                result[groupName] = atomicUnit;\n            }\n\n            return atomicUnit;\n        };\n\n        for (var groupName in tree.UnitConversions) {\n            if (tree.UnitConversions.hasOwnProperty(groupName)) {\n                group = tree.UnitConversions[groupName];\n\n                this.map(mapUnit);\n            }\n        }\n\n        return result;\n    },\n\n    cancel: function () {\n        var counter = {}, atomicUnit, i, backup;\n\n        for (i = 0; i < this.numerator.length; i++) {\n            atomicUnit = this.numerator[i];\n            if (!backup) {\n                backup = atomicUnit;\n            }\n            counter[atomicUnit] = (counter[atomicUnit] || 0) + 1;\n        }\n\n        for (i = 0; i < this.denominator.length; i++) {\n            atomicUnit = this.denominator[i];\n            if (!backup) {\n                backup = atomicUnit;\n            }\n            counter[atomicUnit] = (counter[atomicUnit] || 0) - 1;\n        }\n\n        this.numerator = [];\n        this.denominator = [];\n\n        for (atomicUnit in counter) {\n            if (counter.hasOwnProperty(atomicUnit)) {\n                var count = counter[atomicUnit];\n\n                if (count > 0) {\n                    for (i = 0; i < count; i++) {\n                        this.numerator.push(atomicUnit);\n                    }\n                } else if (count < 0) {\n                    for (i = 0; i < -count; i++) {\n                        this.denominator.push(atomicUnit);\n                    }\n                }\n            }\n        }\n\n        if (this.numerator.length === 0 && this.denominator.length === 0 && backup) {\n            this.backupUnit = backup;\n        }\n\n        this.numerator.sort();\n        this.denominator.sort();\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Directive = function (name, value, rules, index, currentFileInfo, debugInfo) {\n    this.name  = name;\n    this.value = value;\n    if (rules) {\n        this.rules = rules;\n        this.rules.allowImports = true;\n    }\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n    this.debugInfo = debugInfo;\n};\n\ntree.Directive.prototype = {\n    type: \"Directive\",\n    accept: function (visitor) {\n        var value = this.value, rules = this.rules;\n        if (rules) {\n            rules = visitor.visit(rules);\n        }\n        if (value) {\n            value = visitor.visit(value);\n        }\n    },\n    genCSS: function (env, output) {\n        var value = this.value, rules = this.rules;\n        output.add(this.name, this.currentFileInfo, this.index);\n        if (value) {\n            output.add(' ');\n            value.genCSS(env, output);\n        }\n        if (rules) {\n            tree.outputRuleset(env, output, [rules]);\n        } else {\n            output.add(';');\n        }\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        var value = this.value, rules = this.rules;\n        if (value) {\n            value = value.eval(env);\n        }\n        if (rules) {\n            rules = rules.eval(env);\n            rules.root = true;\n        }\n        return new(tree.Directive)(this.name, value, rules,\n            this.index, this.currentFileInfo, this.debugInfo);\n    },\n    variable: function (name) { if (this.rules) return tree.Ruleset.prototype.variable.call(this.rules, name); },\n    find: function () { if (this.rules) return tree.Ruleset.prototype.find.apply(this.rules, arguments); },\n    rulesets: function () { if (this.rules) return tree.Ruleset.prototype.rulesets.apply(this.rules); },\n    markReferenced: function () {\n        var i, rules;\n        this.isReferenced = true;\n        if (this.rules) {\n            rules = this.rules.rules;\n            for (i = 0; i < rules.length; i++) {\n                if (rules[i].markReferenced) {\n                    rules[i].markReferenced();\n                }\n            }\n        }\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Element = function (combinator, value, index, currentFileInfo) {\n    this.combinator = combinator instanceof tree.Combinator ?\n                      combinator : new(tree.Combinator)(combinator);\n\n    if (typeof(value) === 'string') {\n        this.value = value.trim();\n    } else if (value) {\n        this.value = value;\n    } else {\n        this.value = \"\";\n    }\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Element.prototype = {\n    type: \"Element\",\n    accept: function (visitor) {\n        var value = this.value;\n        this.combinator = visitor.visit(this.combinator);\n        if (typeof value === \"object\") {\n            this.value = visitor.visit(value);\n        }\n    },\n    eval: function (env) {\n        return new(tree.Element)(this.combinator,\n                                 this.value.eval ? this.value.eval(env) : this.value,\n                                 this.index,\n                                 this.currentFileInfo);\n    },\n    genCSS: function (env, output) {\n        output.add(this.toCSS(env), this.currentFileInfo, this.index);\n    },\n    toCSS: function (env) {\n        var value = (this.value.toCSS ? this.value.toCSS(env) : this.value);\n        if (value === '' && this.combinator.value.charAt(0) === '&') {\n            return '';\n        } else {\n            return this.combinator.toCSS(env || {}) + value;\n        }\n    }\n};\n\ntree.Attribute = function (key, op, value) {\n    this.key = key;\n    this.op = op;\n    this.value = value;\n};\ntree.Attribute.prototype = {\n    type: \"Attribute\",\n    eval: function (env) {\n        return new(tree.Attribute)(this.key.eval ? this.key.eval(env) : this.key,\n            this.op, (this.value && this.value.eval) ? this.value.eval(env) : this.value);\n    },\n    genCSS: function (env, output) {\n        output.add(this.toCSS(env));\n    },\n    toCSS: function (env) {\n        var value = this.key.toCSS ? this.key.toCSS(env) : this.key;\n\n        if (this.op) {\n            value += this.op;\n            value += (this.value.toCSS ? this.value.toCSS(env) : this.value);\n        }\n\n        return '[' + value + ']';\n    }\n};\n\ntree.Combinator = function (value) {\n    if (value === ' ') {\n        this.value = ' ';\n    } else {\n        this.value = value ? value.trim() : \"\";\n    }\n};\ntree.Combinator.prototype = {\n    type: \"Combinator\",\n    _outputMap: {\n        ''  : '',\n        ' ' : ' ',\n        ':' : ' :',\n        '+' : ' + ',\n        '~' : ' ~ ',\n        '>' : ' > ',\n        '|' : '|',\n        '^' : ' ^ ',\n        '^^' : ' ^^ '\n    },\n    _outputMapCompressed: {\n        ''  : '',\n        ' ' : ' ',\n        ':' : ' :',\n        '+' : '+',\n        '~' : '~',\n        '>' : '>',\n        '|' : '|',\n        '^' : '^',\n        '^^' : '^^'\n    },\n    genCSS: function (env, output) {\n        output.add((env.compress ? this._outputMapCompressed : this._outputMap)[this.value]);\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Expression = function (value) { this.value = value; };\ntree.Expression.prototype = {\n    type: \"Expression\",\n    accept: function (visitor) {\n        if (this.value) {\n            this.value = visitor.visitArray(this.value);\n        }\n    },\n    eval: function (env) {\n        var returnValue,\n            inParenthesis = this.parens && !this.parensInOp,\n            doubleParen = false;\n        if (inParenthesis) {\n            env.inParenthesis();\n        }\n        if (this.value.length > 1) {\n            returnValue = new(tree.Expression)(this.value.map(function (e) {\n                return e.eval(env);\n            }));\n        } else if (this.value.length === 1) {\n            if (this.value[0].parens && !this.value[0].parensInOp) {\n                doubleParen = true;\n            }\n            returnValue = this.value[0].eval(env);\n        } else {\n            returnValue = this;\n        }\n        if (inParenthesis) {\n            env.outOfParenthesis();\n        }\n        if (this.parens && this.parensInOp && !(env.isMathOn()) && !doubleParen) {\n            returnValue = new(tree.Paren)(returnValue);\n        }\n        return returnValue;\n    },\n    genCSS: function (env, output) {\n        for(var i = 0; i < this.value.length; i++) {\n            this.value[i].genCSS(env, output);\n            if (i + 1 < this.value.length) {\n                output.add(\" \");\n            }\n        }\n    },\n    toCSS: tree.toCSS,\n    throwAwayComments: function () {\n        this.value = this.value.filter(function(v) {\n            return !(v instanceof tree.Comment);\n        });\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Extend = function Extend(selector, option, index) {\n    this.selector = selector;\n    this.option = option;\n    this.index = index;\n    this.object_id = tree.Extend.next_id++;\n    this.parent_ids = [this.object_id];\n\n    switch(option) {\n        case \"all\":\n            this.allowBefore = true;\n            this.allowAfter = true;\n        break;\n        default:\n            this.allowBefore = false;\n            this.allowAfter = false;\n        break;\n    }\n};\ntree.Extend.next_id = 0;\n\ntree.Extend.prototype = {\n    type: \"Extend\",\n    accept: function (visitor) {\n        this.selector = visitor.visit(this.selector);\n    },\n    eval: function (env) {\n        return new(tree.Extend)(this.selector.eval(env), this.option, this.index);\n    },\n    clone: function (env) {\n        return new(tree.Extend)(this.selector, this.option, this.index);\n    },\n    findSelfSelectors: function (selectors) {\n        var selfElements = [],\n            i,\n            selectorElements;\n\n        for(i = 0; i < selectors.length; i++) {\n            selectorElements = selectors[i].elements;\n            // duplicate the logic in genCSS function inside the selector node.\n            // future TODO - move both logics into the selector joiner visitor\n            if (i > 0 && selectorElements.length && selectorElements[0].combinator.value === \"\") {\n                selectorElements[0].combinator.value = ' ';\n            }\n            selfElements = selfElements.concat(selectors[i].elements);\n        }\n\n        this.selfSelectors = [{ elements: selfElements }];\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n//\n// CSS @import node\n//\n// The general strategy here is that we don't want to wait\n// for the parsing to be completed, before we start importing\n// the file. That's because in the context of a browser,\n// most of the time will be spent waiting for the server to respond.\n//\n// On creation, we push the import path to our import queue, though\n// `import,push`, we also pass it a callback, which it'll call once\n// the file has been fetched, and parsed.\n//\ntree.Import = function (path, features, options, index, currentFileInfo) {\n    this.options = options;\n    this.index = index;\n    this.path = path;\n    this.features = features;\n    this.currentFileInfo = currentFileInfo;\n\n    if (this.options.less !== undefined || this.options.inline) {\n        this.css = !this.options.less || this.options.inline;\n    } else {\n        var pathValue = this.getPath();\n        if (pathValue && /css([\\?;].*)?$/.test(pathValue)) {\n            this.css = true;\n        }\n    }\n};\n\n//\n// The actual import node doesn't return anything, when converted to CSS.\n// The reason is that it's used at the evaluation stage, so that the rules\n// it imports can be treated like any other rules.\n//\n// In `eval`, we make sure all Import nodes get evaluated, recursively, so\n// we end up with a flat structure, which can easily be imported in the parent\n// ruleset.\n//\ntree.Import.prototype = {\n    type: \"Import\",\n    accept: function (visitor) {\n        if (this.features) {\n            this.features = visitor.visit(this.features);\n        }\n        this.path = visitor.visit(this.path);\n        if (!this.options.inline && this.root) {\n            this.root = visitor.visit(this.root);\n        }\n    },\n    genCSS: function (env, output) {\n        if (this.css) {\n            output.add(\"@import \", this.currentFileInfo, this.index);\n            this.path.genCSS(env, output);\n            if (this.features) {\n                output.add(\" \");\n                this.features.genCSS(env, output);\n            }\n            output.add(';');\n        }\n    },\n    toCSS: tree.toCSS,\n    getPath: function () {\n        if (this.path instanceof tree.Quoted) {\n            var path = this.path.value;\n            return (this.css !== undefined || /(\\.[a-z]*$)|([\\?;].*)$/.test(path)) ? path : path + '.less';\n        } else if (this.path instanceof tree.URL) {\n            return this.path.value.value;\n        }\n        return null;\n    },\n    evalForImport: function (env) {\n        return new(tree.Import)(this.path.eval(env), this.features, this.options, this.index, this.currentFileInfo);\n    },\n    evalPath: function (env) {\n        var path = this.path.eval(env);\n        var rootpath = this.currentFileInfo && this.currentFileInfo.rootpath;\n\n        if (!(path instanceof tree.URL)) {\n            if (rootpath) {\n                var pathValue = path.value;\n                // Add the base path if the import is relative\n                if (pathValue && env.isPathRelative(pathValue)) {\n                    path.value = rootpath +pathValue;\n                }\n            }\n            path.value = env.normalizePath(path.value);\n        }\n\n        return path;\n    },\n    eval: function (env) {\n        var ruleset, features = this.features && this.features.eval(env);\n\n        if (this.skip) {\n            if (typeof this.skip === \"function\") {\n                this.skip = this.skip();\n            }\n            if (this.skip) {\n                return []; \n            }\n        }\n         \n        if (this.options.inline) {\n            //todo needs to reference css file not import\n            var contents = new(tree.Anonymous)(this.root, 0, {filename: this.importedFilename}, true);\n            return this.features ? new(tree.Media)([contents], this.features.value) : [contents];\n        } else if (this.css) {\n            var newImport = new(tree.Import)(this.evalPath(env), features, this.options, this.index);\n            if (!newImport.css && this.error) {\n                throw this.error;\n            }\n            return newImport;\n        } else {\n            ruleset = new(tree.Ruleset)(null, this.root.rules.slice(0));\n\n            ruleset.evalImports(env);\n\n            return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules;\n        }\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.JavaScript = function (string, index, escaped) {\n    this.escaped = escaped;\n    this.expression = string;\n    this.index = index;\n};\ntree.JavaScript.prototype = {\n    type: \"JavaScript\",\n    eval: function (env) {\n        var result,\n            that = this,\n            context = {};\n\n        var expression = this.expression.replace(/@\\{([\\w-]+)\\}/g, function (_, name) {\n            return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env));\n        });\n\n        try {\n            expression = new(Function)('return (' + expression + ')');\n        } catch (e) {\n            throw { message: \"JavaScript evaluation error: \" + e.message + \" from `\" + expression + \"`\" ,\n                    index: this.index };\n        }\n\n        var variables = env.frames[0].variables();\n        for (var k in variables) {\n            if (variables.hasOwnProperty(k)) {\n                /*jshint loopfunc:true */\n                context[k.slice(1)] = {\n                    value: variables[k].value,\n                    toJS: function () {\n                        return this.value.eval(env).toCSS();\n                    }\n                };\n            }\n        }\n\n        try {\n            result = expression.call(context);\n        } catch (e) {\n            throw { message: \"JavaScript evaluation error: '\" + e.name + ': ' + e.message.replace(/[\"]/g, \"'\") + \"'\" ,\n                    index: this.index };\n        }\n        if (typeof(result) === 'number') {\n            return new(tree.Dimension)(result);\n        } else if (typeof(result) === 'string') {\n            return new(tree.Quoted)('\"' + result + '\"', result, this.escaped, this.index);\n        } else if (Array.isArray(result)) {\n            return new(tree.Anonymous)(result.join(', '));\n        } else {\n            return new(tree.Anonymous)(result);\n        }\n    }\n};\n\n})(require('../tree'));\n\n\n(function (tree) {\n\ntree.Keyword = function (value) { this.value = value; };\ntree.Keyword.prototype = {\n    type: \"Keyword\",\n    eval: function () { return this; },\n    genCSS: function (env, output) {\n        if (this.value === '%') { throw { type: \"Syntax\", message: \"Invalid % without number\" }; }\n        output.add(this.value);\n    },\n    toCSS: tree.toCSS,\n    compare: function (other) {\n        if (other instanceof tree.Keyword) {\n            return other.value === this.value ? 0 : 1;\n        } else {\n            return -1;\n        }\n    }\n};\n\ntree.True = new(tree.Keyword)('true');\ntree.False = new(tree.Keyword)('false');\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Media = function (value, features, index, currentFileInfo) {\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n\n    var selectors = this.emptySelectors();\n\n    this.features = new(tree.Value)(features);\n    this.rules = [new(tree.Ruleset)(selectors, value)];\n    this.rules[0].allowImports = true;\n};\ntree.Media.prototype = {\n    type: \"Media\",\n    accept: function (visitor) {\n        if (this.features) {\n            this.features = visitor.visit(this.features);\n        }\n        if (this.rules) {\n            this.rules = visitor.visitArray(this.rules);\n        }\n    },\n    genCSS: function (env, output) {\n        output.add('@media ', this.currentFileInfo, this.index);\n        this.features.genCSS(env, output);\n        tree.outputRuleset(env, output, this.rules);\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        if (!env.mediaBlocks) {\n            env.mediaBlocks = [];\n            env.mediaPath = [];\n        }\n        \n        var media = new(tree.Media)(null, [], this.index, this.currentFileInfo);\n        if(this.debugInfo) {\n            this.rules[0].debugInfo = this.debugInfo;\n            media.debugInfo = this.debugInfo;\n        }\n        var strictMathBypass = false;\n        if (!env.strictMath) {\n            strictMathBypass = true;\n            env.strictMath = true;\n        }\n        try {\n            media.features = this.features.eval(env);\n        }\n        finally {\n            if (strictMathBypass) {\n                env.strictMath = false;\n            }\n        }\n        \n        env.mediaPath.push(media);\n        env.mediaBlocks.push(media);\n        \n        env.frames.unshift(this.rules[0]);\n        media.rules = [this.rules[0].eval(env)];\n        env.frames.shift();\n        \n        env.mediaPath.pop();\n\n        return env.mediaPath.length === 0 ? media.evalTop(env) :\n                    media.evalNested(env);\n    },\n    variable: function (name) { return tree.Ruleset.prototype.variable.call(this.rules[0], name); },\n    find: function () { return tree.Ruleset.prototype.find.apply(this.rules[0], arguments); },\n    rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.rules[0]); },\n    emptySelectors: function() { \n        var el = new(tree.Element)('', '&', this.index, this.currentFileInfo),\n            sels = [new(tree.Selector)([el], null, null, this.index, this.currentFileInfo)];\n        sels[0].mediaEmpty = true;\n        return sels;\n    },\n    markReferenced: function () {\n        var i, rules = this.rules[0].rules;\n        this.rules[0].markReferenced();\n        this.isReferenced = true;\n        for (i = 0; i < rules.length; i++) {\n            if (rules[i].markReferenced) {\n                rules[i].markReferenced();\n            }\n        }\n    },\n\n    evalTop: function (env) {\n        var result = this;\n\n        // Render all dependent Media blocks.\n        if (env.mediaBlocks.length > 1) {\n            var selectors = this.emptySelectors();\n            result = new(tree.Ruleset)(selectors, env.mediaBlocks);\n            result.multiMedia = true;\n        }\n\n        delete env.mediaBlocks;\n        delete env.mediaPath;\n\n        return result;\n    },\n    evalNested: function (env) {\n        var i, value,\n            path = env.mediaPath.concat([this]);\n\n        // Extract the media-query conditions separated with `,` (OR).\n        for (i = 0; i < path.length; i++) {\n            value = path[i].features instanceof tree.Value ?\n                        path[i].features.value : path[i].features;\n            path[i] = Array.isArray(value) ? value : [value];\n        }\n\n        // Trace all permutations to generate the resulting media-query.\n        //\n        // (a, b and c) with nested (d, e) ->\n        //    a and d\n        //    a and e\n        //    b and c and d\n        //    b and c and e\n        this.features = new(tree.Value)(this.permute(path).map(function (path) {\n            path = path.map(function (fragment) {\n                return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment);\n            });\n\n            for(i = path.length - 1; i > 0; i--) {\n                path.splice(i, 0, new(tree.Anonymous)(\"and\"));\n            }\n\n            return new(tree.Expression)(path);\n        }));\n\n        // Fake a tree-node that doesn't output anything.\n        return new(tree.Ruleset)([], []);\n    },\n    permute: function (arr) {\n      if (arr.length === 0) {\n          return [];\n      } else if (arr.length === 1) {\n          return arr[0];\n      } else {\n          var result = [];\n          var rest = this.permute(arr.slice(1));\n          for (var i = 0; i < rest.length; i++) {\n              for (var j = 0; j < arr[0].length; j++) {\n                  result.push([arr[0][j]].concat(rest[i]));\n              }\n          }\n          return result;\n      }\n    },\n    bubbleSelectors: function (selectors) {\n      if (!selectors)\n        return;\n      this.rules = [new(tree.Ruleset)(selectors.slice(0), [this.rules[0]])];\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.mixin = {};\ntree.mixin.Call = function (elements, args, index, currentFileInfo, important) {\n    this.selector = new(tree.Selector)(elements);\n    this.arguments = (args && args.length) ? args : null;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n    this.important = important;\n};\ntree.mixin.Call.prototype = {\n    type: \"MixinCall\",\n    accept: function (visitor) {\n        if (this.selector) {\n            this.selector = visitor.visit(this.selector);\n        }\n        if (this.arguments) {\n            this.arguments = visitor.visitArray(this.arguments);\n        }\n    },\n    eval: function (env) {\n        var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound, rule,\n            candidates = [], candidate, conditionResult = [], defaultFunc = tree.defaultFunc,\n            defaultResult, defNone = 0, defTrue = 1, defFalse = 2, count; \n\n        args = this.arguments && this.arguments.map(function (a) {\n            return { name: a.name, value: a.value.eval(env) };\n        });\n\n        for (i = 0; i < env.frames.length; i++) {\n            if ((mixins = env.frames[i].find(this.selector)).length > 0) {\n                isOneFound = true;\n                \n                // To make `default()` function independent of definition order we have two \"subpasses\" here.\n                // At first we evaluate each guard *twice* (with `default() == true` and `default() == false`),\n                // and build candidate list with corresponding flags. Then, when we know all possible matches,\n                // we make a final decision.\n                \n                for (m = 0; m < mixins.length; m++) {\n                    mixin = mixins[m];\n                    isRecursive = false;\n                    for(f = 0; f < env.frames.length; f++) {\n                        if ((!(mixin instanceof tree.mixin.Definition)) && mixin === (env.frames[f].originalRuleset || env.frames[f])) {\n                            isRecursive = true;\n                            break;\n                        }\n                    }\n                    if (isRecursive) {\n                        continue;\n                    }\n                    \n                    if (mixin.matchArgs(args, env)) {  \n                        candidate = {mixin: mixin, group: defNone};\n                        \n                        if (mixin.matchCondition) { \n                            for (f = 0; f < 2; f++) {\n                                defaultFunc.value(f);\n                                conditionResult[f] = mixin.matchCondition(args, env);\n                            }\n                            if (conditionResult[0] || conditionResult[1]) {\n                                if (conditionResult[0] != conditionResult[1]) {\n                                    candidate.group = conditionResult[1] ?\n                                        defTrue : defFalse;\n                                }\n\n                                candidates.push(candidate);\n                            }   \n                        }\n                        else {\n                            candidates.push(candidate);\n                        }\n                        \n                        match = true;\n                    }\n                }\n                \n                defaultFunc.reset();\n\n                count = [0, 0, 0];\n                for (m = 0; m < candidates.length; m++) {\n                    count[candidates[m].group]++;\n                }\n\n                if (count[defNone] > 0) {\n                    defaultResult = defFalse;\n                } else {\n                    defaultResult = defTrue;\n                    if ((count[defTrue] + count[defFalse]) > 1) {\n                        throw { type: 'Runtime',\n                            message: 'Ambiguous use of `default()` found when matching for `'\n                                + this.format(args) + '`',\n                            index: this.index, filename: this.currentFileInfo.filename };\n                    }\n                }\n                \n                for (m = 0; m < candidates.length; m++) {\n                    candidate = candidates[m].group;\n                    if ((candidate === defNone) || (candidate === defaultResult)) {\n                        try {\n                            mixin = candidates[m].mixin;\n                            if (!(mixin instanceof tree.mixin.Definition)) {\n                                mixin = new tree.mixin.Definition(\"\", [], mixin.rules, null, false);\n                                mixin.originalRuleset = mixins[m].originalRuleset || mixins[m];\n                            }\n                            Array.prototype.push.apply(\n                                  rules, mixin.evalCall(env, args, this.important).rules);\n                        } catch (e) {\n                            throw { message: e.message, index: this.index, filename: this.currentFileInfo.filename, stack: e.stack };\n                        }\n                    }\n                }\n                \n                if (match) {\n                    if (!this.currentFileInfo || !this.currentFileInfo.reference) {\n                        for (i = 0; i < rules.length; i++) {\n                            rule = rules[i];\n                            if (rule.markReferenced) {\n                                rule.markReferenced();\n                            }\n                        }\n                    }\n                    return rules;\n                }\n            }\n        }\n        if (isOneFound) {\n            throw { type:    'Runtime',\n                    message: 'No matching definition was found for `' + this.format(args) + '`',\n                    index:   this.index, filename: this.currentFileInfo.filename };\n        } else {\n            throw { type:    'Name',\n                    message: this.selector.toCSS().trim() + \" is undefined\",\n                    index:   this.index, filename: this.currentFileInfo.filename };\n        }\n    },\n    format: function (args) {\n        return this.selector.toCSS().trim() + '(' +\n            (args ? args.map(function (a) {\n                var argValue = \"\";\n                if (a.name) {\n                    argValue += a.name + \":\";\n                }\n                if (a.value.toCSS) {\n                    argValue += a.value.toCSS();\n                } else {\n                    argValue += \"???\";\n                }\n                return argValue;\n            }).join(', ') : \"\") + \")\";\n    }\n};\n\ntree.mixin.Definition = function (name, params, rules, condition, variadic, frames) {\n    this.name = name;\n    this.selectors = [new(tree.Selector)([new(tree.Element)(null, name, this.index, this.currentFileInfo)])];\n    this.params = params;\n    this.condition = condition;\n    this.variadic = variadic;\n    this.arity = params.length;\n    this.rules = rules;\n    this._lookups = {};\n    this.required = params.reduce(function (count, p) {\n        if (!p.name || (p.name && !p.value)) { return count + 1; }\n        else                                 { return count; }\n    }, 0);\n    this.parent = tree.Ruleset.prototype;\n    this.frames = frames;\n};\ntree.mixin.Definition.prototype = {\n    type: \"MixinDefinition\",\n    accept: function (visitor) {\n        if (this.params && this.params.length) {\n            this.params = visitor.visitArray(this.params);\n        }\n        this.rules = visitor.visitArray(this.rules);\n        if (this.condition) {\n            this.condition = visitor.visit(this.condition);\n        }\n    },\n    variable:  function (name) { return this.parent.variable.call(this, name); },\n    variables: function ()     { return this.parent.variables.call(this); },\n    find:      function ()     { return this.parent.find.apply(this, arguments); },\n    rulesets:  function ()     { return this.parent.rulesets.apply(this); },\n\n    evalParams: function (env, mixinEnv, args, evaldArguments) {\n        /*jshint boss:true */\n        var frame = new(tree.Ruleset)(null, null),\n            varargs, arg,\n            params = this.params.slice(0),\n            i, j, val, name, isNamedFound, argIndex, argsLength = 0;\n\n        mixinEnv = new tree.evalEnv(mixinEnv, [frame].concat(mixinEnv.frames));\n\n        if (args) {\n            args = args.slice(0);\n            argsLength = args.length;\n\n            for(i = 0; i < argsLength; i++) {\n                arg = args[i];\n                if (name = (arg && arg.name)) {\n                    isNamedFound = false;\n                    for(j = 0; j < params.length; j++) {\n                        if (!evaldArguments[j] && name === params[j].name) {\n                            evaldArguments[j] = arg.value.eval(env);\n                            frame.prependRule(new(tree.Rule)(name, arg.value.eval(env)));\n                            isNamedFound = true;\n                            break;\n                        }\n                    }\n                    if (isNamedFound) {\n                        args.splice(i, 1);\n                        i--;\n                        continue;\n                    } else {\n                        throw { type: 'Runtime', message: \"Named argument for \" + this.name +\n                            ' ' + args[i].name + ' not found' };\n                    }\n                }\n            }\n        }\n        argIndex = 0;\n        for (i = 0; i < params.length; i++) {\n            if (evaldArguments[i]) { continue; }\n\n            arg = args && args[argIndex];\n\n            if (name = params[i].name) {\n                if (params[i].variadic) {\n                    varargs = [];\n                    for (j = argIndex; j < argsLength; j++) {\n                        varargs.push(args[j].value.eval(env));\n                    }\n                    frame.prependRule(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env)));\n                } else {\n                    val = arg && arg.value;\n                    if (val) {\n                        val = val.eval(env);\n                    } else if (params[i].value) {\n                        val = params[i].value.eval(mixinEnv);\n                        frame.resetCache();\n                    } else {\n                        throw { type: 'Runtime', message: \"wrong number of arguments for \" + this.name +\n                            ' (' + argsLength + ' for ' + this.arity + ')' };\n                    }\n                    \n                    frame.prependRule(new(tree.Rule)(name, val));\n                    evaldArguments[i] = val;\n                }\n            }\n\n            if (params[i].variadic && args) {\n                for (j = argIndex; j < argsLength; j++) {\n                    evaldArguments[j] = args[j].value.eval(env);\n                }\n            }\n            argIndex++;\n        }\n\n        return frame;\n    },\n    eval: function (env) {\n        return new tree.mixin.Definition(this.name, this.params, this.rules, this.condition, this.variadic, this.frames || env.frames.slice(0));\n    },\n    evalCall: function (env, args, important) {\n        var _arguments = [],\n            mixinFrames = this.frames ? this.frames.concat(env.frames) : env.frames,\n            frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments),\n            rules, ruleset;\n\n        frame.prependRule(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env)));\n\n        rules = this.rules.slice(0);\n\n        ruleset = new(tree.Ruleset)(null, rules);\n        ruleset.originalRuleset = this;\n        ruleset = ruleset.eval(new(tree.evalEnv)(env, [this, frame].concat(mixinFrames)));\n        if (important) {\n            ruleset = this.parent.makeImportant.apply(ruleset);\n        }\n        return ruleset;\n    },\n    matchCondition: function (args, env) {\n        if (this.condition && !this.condition.eval(\n            new(tree.evalEnv)(env,\n                [this.evalParams(env, new(tree.evalEnv)(env, this.frames.concat(env.frames)), args, [])] // the parameter variables\n                    .concat(this.frames) // the parent namespace/mixin frames\n                    .concat(env.frames)))) { // the current environment frames\n            return false;\n        }\n        return true;\n    },\n    matchArgs: function (args, env) {\n        var argsLength = (args && args.length) || 0, len;\n\n        if (! this.variadic) {\n            if (argsLength < this.required)                               { return false; }\n            if (argsLength > this.params.length)                          { return false; }\n        } else {\n            if (argsLength < (this.required - 1))                         { return false; }\n        }\n\n        len = Math.min(argsLength, this.arity);\n\n        for (var i = 0; i < len; i++) {\n            if (!this.params[i].name && !this.params[i].variadic) {\n                if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Negative = function (node) {\n    this.value = node;\n};\ntree.Negative.prototype = {\n    type: \"Negative\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    genCSS: function (env, output) {\n        output.add('-');\n        this.value.genCSS(env, output);\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        if (env.isMathOn()) {\n            return (new(tree.Operation)('*', [new(tree.Dimension)(-1), this.value])).eval(env);\n        }\n        return new(tree.Negative)(this.value.eval(env));\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Operation = function (op, operands, isSpaced) {\n    this.op = op.trim();\n    this.operands = operands;\n    this.isSpaced = isSpaced;\n};\ntree.Operation.prototype = {\n    type: \"Operation\",\n    accept: function (visitor) {\n        this.operands = visitor.visit(this.operands);\n    },\n    eval: function (env) {\n        var a = this.operands[0].eval(env),\n            b = this.operands[1].eval(env);\n\n        if (env.isMathOn()) {\n            if (a instanceof tree.Dimension && b instanceof tree.Color) {\n                a = a.toColor();\n            }\n            if (b instanceof tree.Dimension && a instanceof tree.Color) {\n                b = b.toColor();\n            }\n            if (!a.operate) {\n                throw { type: \"Operation\",\n                        message: \"Operation on an invalid type\" };\n            }\n\n            return a.operate(env, this.op, b);\n        } else {\n            return new(tree.Operation)(this.op, [a, b], this.isSpaced);\n        }\n    },\n    genCSS: function (env, output) {\n        this.operands[0].genCSS(env, output);\n        if (this.isSpaced) {\n            output.add(\" \");\n        }\n        output.add(this.op);\n        if (this.isSpaced) {\n            output.add(\" \");\n        }\n        this.operands[1].genCSS(env, output);\n    },\n    toCSS: tree.toCSS\n};\n\ntree.operate = function (env, op, a, b) {\n    switch (op) {\n        case '+': return a + b;\n        case '-': return a - b;\n        case '*': return a * b;\n        case '/': return a / b;\n    }\n};\n\n})(require('../tree'));\n\n\n(function (tree) {\n\ntree.Paren = function (node) {\n    this.value = node;\n};\ntree.Paren.prototype = {\n    type: \"Paren\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    genCSS: function (env, output) {\n        output.add('(');\n        this.value.genCSS(env, output);\n        output.add(')');\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        return new(tree.Paren)(this.value.eval(env));\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Quoted = function (str, content, escaped, index, currentFileInfo) {\n    this.escaped = escaped;\n    this.value = content || '';\n    this.quote = str.charAt(0);\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n};\ntree.Quoted.prototype = {\n    type: \"Quoted\",\n    genCSS: function (env, output) {\n        if (!this.escaped) {\n            output.add(this.quote, this.currentFileInfo, this.index);\n        }\n        output.add(this.value);\n        if (!this.escaped) {\n            output.add(this.quote);\n        }\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        var that = this;\n        var value = this.value.replace(/`([^`]+)`/g, function (_, exp) {\n            return new(tree.JavaScript)(exp, that.index, true).eval(env).value;\n        }).replace(/@\\{([\\w-]+)\\}/g, function (_, name) {\n            var v = new(tree.Variable)('@' + name, that.index, that.currentFileInfo).eval(env, true);\n            return (v instanceof tree.Quoted) ? v.value : v.toCSS();\n        });\n        return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index, this.currentFileInfo);\n    },\n    compare: function (x) {\n        if (!x.toCSS) {\n            return -1;\n        }\n        \n        var left = this.toCSS(),\n            right = x.toCSS();\n        \n        if (left === right) {\n            return 0;\n        }\n        \n        return left < right ? -1 : 1;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Rule = function (name, value, important, merge, index, currentFileInfo, inline) {\n    this.name = name;\n    this.value = (value instanceof tree.Value || value instanceof tree.Ruleset) ? value : new(tree.Value)([value]);\n    this.important = important ? ' ' + important.trim() : '';\n    this.merge = merge;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo;\n    this.inline = inline || false;\n    this.variable = name.charAt && (name.charAt(0) === '@');\n};\n\ntree.Rule.prototype = {\n    type: \"Rule\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    genCSS: function (env, output) {\n        output.add(this.name + (env.compress ? ':' : ': '), this.currentFileInfo, this.index);\n        try {\n            this.value.genCSS(env, output);\n        }\n        catch(e) {\n            e.index = this.index;\n            e.filename = this.currentFileInfo.filename;\n            throw e;\n        }\n        output.add(this.important + ((this.inline || (env.lastRule && env.compress)) ? \"\" : \";\"), this.currentFileInfo, this.index);\n    },\n    toCSS: tree.toCSS,\n    eval: function (env) {\n        var strictMathBypass = false, name = this.name, evaldValue;\n        if (typeof name !== \"string\") {\n            // expand 'primitive' name directly to get\n            // things faster (~10% for benchmark.less):\n            name = (name.length === 1) \n                && (name[0] instanceof tree.Keyword)\n                    ? name[0].value : evalName(env, name);\n        }\n        if (name === \"font\" && !env.strictMath) {\n            strictMathBypass = true;\n            env.strictMath = true;\n        }\n        try {\n            evaldValue = this.value.eval(env);\n            \n            if (!this.variable && evaldValue.type === \"DetachedRuleset\") {\n                throw { message: \"Rulesets cannot be evaluated on a property.\",\n                        index: this.index, filename: this.currentFileInfo.filename };\n            }\n\n            return new(tree.Rule)(name,\n                              evaldValue,\n                              this.important,\n                              this.merge,\n                              this.index, this.currentFileInfo, this.inline);\n        }\n        catch(e) {\n            if (typeof e.index !== 'number') {\n                e.index = this.index;\n                e.filename = this.currentFileInfo.filename;\n            }\n            throw e;\n        }\n        finally {\n            if (strictMathBypass) {\n                env.strictMath = false;\n            }\n        }\n    },\n    makeImportant: function () {\n        return new(tree.Rule)(this.name,\n                              this.value,\n                              \"!important\",\n                              this.merge,\n                              this.index, this.currentFileInfo, this.inline);\n    }\n};\n\nfunction evalName(env, name) {\n    var value = \"\", i, n = name.length,\n        output = {add: function (s) {value += s;}};\n    for (i = 0; i < n; i++) {\n        name[i].eval(env).genCSS(env, output);\n    }\n    return value;\n}\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.RulesetCall = function (variable) {\n    this.variable = variable;\n};\ntree.RulesetCall.prototype = {\n    type: \"RulesetCall\",\n    accept: function (visitor) {\n    },\n    eval: function (env) {\n        var detachedRuleset = new(tree.Variable)(this.variable).eval(env);\n        return detachedRuleset.callEval(env);\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Ruleset = function (selectors, rules, strictImports) {\n    this.selectors = selectors;\n    this.rules = rules;\n    this._lookups = {};\n    this.strictImports = strictImports;\n};\ntree.Ruleset.prototype = {\n    type: \"Ruleset\",\n    accept: function (visitor) {\n        if (this.paths) {\n            visitor.visitArray(this.paths, true);\n        } else if (this.selectors) {\n            this.selectors = visitor.visitArray(this.selectors);\n        }\n        if (this.rules && this.rules.length) {\n            this.rules = visitor.visitArray(this.rules);\n        }\n    },\n    eval: function (env) {\n        var thisSelectors = this.selectors, selectors, \n            selCnt, selector, i, defaultFunc = tree.defaultFunc, hasOnePassingSelector = false;\n\n        if (thisSelectors && (selCnt = thisSelectors.length)) {\n            selectors = [];\n            defaultFunc.error({\n                type: \"Syntax\", \n                message: \"it is currently only allowed in parametric mixin guards,\" \n            });\n            for (i = 0; i < selCnt; i++) {\n                selector = thisSelectors[i].eval(env);\n                selectors.push(selector);\n                if (selector.evaldCondition) {\n                    hasOnePassingSelector = true;\n                }\n            }\n            defaultFunc.reset();  \n        } else {\n            hasOnePassingSelector = true;\n        }\n\n        var rules = this.rules ? this.rules.slice(0) : null,\n            ruleset = new(tree.Ruleset)(selectors, rules, this.strictImports),\n            rule, subRule;\n\n        ruleset.originalRuleset = this;\n        ruleset.root = this.root;\n        ruleset.firstRoot = this.firstRoot;\n        ruleset.allowImports = this.allowImports;\n\n        if(this.debugInfo) {\n            ruleset.debugInfo = this.debugInfo;\n        }\n        \n        if (!hasOnePassingSelector) {\n            rules.length = 0;\n        }\n\n        // push the current ruleset to the frames stack\n        var envFrames = env.frames;\n        envFrames.unshift(ruleset);\n\n        // currrent selectors\n        var envSelectors = env.selectors;\n        if (!envSelectors) {\n            env.selectors = envSelectors = [];\n        }\n        envSelectors.unshift(this.selectors);\n\n        // Evaluate imports\n        if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) {\n            ruleset.evalImports(env);\n        }\n\n        // Store the frames around mixin definitions,\n        // so they can be evaluated like closures when the time comes.\n        var rsRules = ruleset.rules, rsRuleCnt = rsRules ? rsRules.length : 0;\n        for (i = 0; i < rsRuleCnt; i++) {\n            if (rsRules[i] instanceof tree.mixin.Definition || rsRules[i] instanceof tree.DetachedRuleset) {\n                rsRules[i] = rsRules[i].eval(env);\n            }\n        }\n\n        var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0;\n\n        // Evaluate mixin calls.\n        for (i = 0; i < rsRuleCnt; i++) {\n            if (rsRules[i] instanceof tree.mixin.Call) {\n                /*jshint loopfunc:true */\n                rules = rsRules[i].eval(env).filter(function(r) {\n                    if ((r instanceof tree.Rule) && r.variable) {\n                        // do not pollute the scope if the variable is\n                        // already there. consider returning false here\n                        // but we need a way to \"return\" variable from mixins\n                        return !(ruleset.variable(r.name));\n                    }\n                    return true;\n                });\n                rsRules.splice.apply(rsRules, [i, 1].concat(rules));\n                rsRuleCnt += rules.length - 1;\n                i += rules.length-1;\n                ruleset.resetCache();\n            } else if (rsRules[i] instanceof tree.RulesetCall) {\n                /*jshint loopfunc:true */\n                rules = rsRules[i].eval(env).rules.filter(function(r) {\n                    if ((r instanceof tree.Rule) && r.variable) {\n                        // do not pollute the scope at all\n                        return false;\n                    }\n                    return true;\n                });\n                rsRules.splice.apply(rsRules, [i, 1].concat(rules));\n                rsRuleCnt += rules.length - 1;\n                i += rules.length-1;\n                ruleset.resetCache();\n            }\n        }\n\n        // Evaluate everything else\n        for (i = 0; i < rsRules.length; i++) {\n            rule = rsRules[i];\n            if (! (rule instanceof tree.mixin.Definition || rule instanceof tree.DetachedRuleset)) {\n                rsRules[i] = rule = rule.eval ? rule.eval(env) : rule;\n            }\n        }\n        \n        // Evaluate everything else\n        for (i = 0; i < rsRules.length; i++) {\n            rule = rsRules[i];\n            // for rulesets, check if it is a css guard and can be removed\n            if (rule instanceof tree.Ruleset && rule.selectors && rule.selectors.length === 1) {\n                // check if it can be folded in (e.g. & where)\n                if (rule.selectors[0].isJustParentSelector()) {\n                    rsRules.splice(i--, 1);\n\n                    for(var j = 0; j < rule.rules.length; j++) {\n                        subRule = rule.rules[j];\n                        if (!(subRule instanceof tree.Rule) || !subRule.variable) {\n                            rsRules.splice(++i, 0, subRule);\n                        }\n                    }\n                }\n            }\n        }\n\n        // Pop the stack\n        envFrames.shift();\n        envSelectors.shift();\n        \n        if (env.mediaBlocks) {\n            for (i = mediaBlockCount; i < env.mediaBlocks.length; i++) {\n                env.mediaBlocks[i].bubbleSelectors(selectors);\n            }\n        }\n\n        return ruleset;\n    },\n    evalImports: function(env) {\n        var rules = this.rules, i, importRules;\n        if (!rules) { return; }\n\n        for (i = 0; i < rules.length; i++) {\n            if (rules[i] instanceof tree.Import) {\n                importRules = rules[i].eval(env);\n                if (importRules && importRules.length) {\n                    rules.splice.apply(rules, [i, 1].concat(importRules));\n                    i+= importRules.length-1;\n                } else {\n                    rules.splice(i, 1, importRules);\n                }\n                this.resetCache();\n            }\n        }\n    },\n    makeImportant: function() {\n        return new tree.Ruleset(this.selectors, this.rules.map(function (r) {\n                    if (r.makeImportant) {\n                        return r.makeImportant();\n                    } else {\n                        return r;\n                    }\n                }), this.strictImports);\n    },\n    matchArgs: function (args) {\n        return !args || args.length === 0;\n    },\n    // lets you call a css selector with a guard\n    matchCondition: function (args, env) {\n        var lastSelector = this.selectors[this.selectors.length-1];\n        if (!lastSelector.evaldCondition) {\n            return false;\n        }\n        if (lastSelector.condition &&\n            !lastSelector.condition.eval(\n                new(tree.evalEnv)(env,\n                    env.frames))) {\n            return false;\n        }\n        return true;\n    },\n    resetCache: function () {\n        this._rulesets = null;\n        this._variables = null;\n        this._lookups = {};\n    },\n    variables: function () {\n        if (!this._variables) {\n            this._variables = !this.rules ? {} : this.rules.reduce(function (hash, r) {\n                if (r instanceof tree.Rule && r.variable === true) {\n                    hash[r.name] = r;\n                }\n                return hash;\n            }, {});\n        }\n        return this._variables;\n    },\n    variable: function (name) {\n        return this.variables()[name];\n    },\n    rulesets: function () {\n        if (!this.rules) { return null; }\n\n        var _Ruleset = tree.Ruleset, _MixinDefinition = tree.mixin.Definition,\n            filtRules = [], rules = this.rules, cnt = rules.length,\n            i, rule;\n\n        for (i = 0; i < cnt; i++) {\n            rule = rules[i];\n            if ((rule instanceof _Ruleset) || (rule instanceof _MixinDefinition)) {\n                filtRules.push(rule);\n            }\n        }\n\n        return filtRules;\n    },\n    prependRule: function (rule) {\n        var rules = this.rules;\n        if (rules) { rules.unshift(rule); } else { this.rules = [ rule ]; }\n    },\n    find: function (selector, self) {\n        self = self || this;\n        var rules = [], match,\n            key = selector.toCSS();\n\n        if (key in this._lookups) { return this._lookups[key]; }\n\n        this.rulesets().forEach(function (rule) {\n            if (rule !== self) {\n                for (var j = 0; j < rule.selectors.length; j++) {\n                    match = selector.match(rule.selectors[j]);\n                    if (match) {\n                        if (selector.elements.length > match) {\n                            Array.prototype.push.apply(rules, rule.find(\n                                new(tree.Selector)(selector.elements.slice(match)), self));\n                        } else {\n                            rules.push(rule);\n                        }\n                        break;\n                    }\n                }\n            }\n        });\n        this._lookups[key] = rules;\n        return rules;\n    },\n    genCSS: function (env, output) {\n        var i, j,\n            ruleNodes = [],\n            rulesetNodes = [],\n            rulesetNodeCnt,\n            debugInfo,     // Line number debugging\n            rule,\n            path;\n\n        env.tabLevel = (env.tabLevel || 0);\n\n        if (!this.root) {\n            env.tabLevel++;\n        }\n\n        var tabRuleStr = env.compress ? '' : Array(env.tabLevel + 1).join(\"  \"),\n            tabSetStr = env.compress ? '' : Array(env.tabLevel).join(\"  \"),\n            sep;\n\n        for (i = 0; i < this.rules.length; i++) {\n            rule = this.rules[i];\n            if (rule.rules || (rule instanceof tree.Media) || rule instanceof tree.Directive || (this.root && rule instanceof tree.Comment)) {\n                rulesetNodes.push(rule);\n            } else {\n                ruleNodes.push(rule);\n            }\n        }\n\n        // If this is the root node, we don't render\n        // a selector, or {}.\n        if (!this.root) {\n            debugInfo = tree.debugInfo(env, this, tabSetStr);\n\n            if (debugInfo) {\n                output.add(debugInfo);\n                output.add(tabSetStr);\n            }\n\n            var paths = this.paths, pathCnt = paths.length,\n                pathSubCnt;\n\n            sep = env.compress ? ',' : (',\\n' + tabSetStr);\n\n            for (i = 0; i < pathCnt; i++) {\n                path = paths[i];\n                if (!(pathSubCnt = path.length)) { continue; }\n                if (i > 0) { output.add(sep); }\n\n                env.firstSelector = true;\n                path[0].genCSS(env, output);\n\n                env.firstSelector = false;\n                for (j = 1; j < pathSubCnt; j++) {\n                    path[j].genCSS(env, output);\n                }\n            }\n\n            output.add((env.compress ? '{' : ' {\\n') + tabRuleStr);\n        }\n\n        // Compile rules and rulesets\n        for (i = 0; i < ruleNodes.length; i++) {\n            rule = ruleNodes[i];\n\n            // @page{ directive ends up with root elements inside it, a mix of rules and rulesets\n            // In this instance we do not know whether it is the last property\n            if (i + 1 === ruleNodes.length && (!this.root || rulesetNodes.length === 0 || this.firstRoot)) {\n                env.lastRule = true;\n            }\n\n            if (rule.genCSS) {\n                rule.genCSS(env, output);\n            } else if (rule.value) {\n                output.add(rule.value.toString());\n            }\n\n            if (!env.lastRule) {\n                output.add(env.compress ? '' : ('\\n' + tabRuleStr));\n            } else {\n                env.lastRule = false;\n            }\n        }\n\n        if (!this.root) {\n            output.add((env.compress ? '}' : '\\n' + tabSetStr + '}'));\n            env.tabLevel--;\n        }\n\n        sep = (env.compress ? \"\" : \"\\n\") + (this.root ? tabRuleStr : tabSetStr);\n        rulesetNodeCnt = rulesetNodes.length;\n        if (rulesetNodeCnt) {\n            if (ruleNodes.length && sep) { output.add(sep); }\n            rulesetNodes[0].genCSS(env, output);\n            for (i = 1; i < rulesetNodeCnt; i++) {\n                if (sep) { output.add(sep); }\n                rulesetNodes[i].genCSS(env, output);\n            }\n        }\n\n        if (!output.isEmpty() && !env.compress && this.firstRoot) {\n            output.add('\\n');\n        }\n    },\n\n    toCSS: tree.toCSS,\n\n    markReferenced: function () {\n        if (!this.selectors) {\n            return;\n        }\n        for (var s = 0; s < this.selectors.length; s++) {\n            this.selectors[s].markReferenced();\n        }\n    },\n\n    joinSelectors: function (paths, context, selectors) {\n        for (var s = 0; s < selectors.length; s++) {\n            this.joinSelector(paths, context, selectors[s]);\n        }\n    },\n\n    joinSelector: function (paths, context, selector) {\n\n        var i, j, k, \n            hasParentSelector, newSelectors, el, sel, parentSel, \n            newSelectorPath, afterParentJoin, newJoinedSelector, \n            newJoinedSelectorEmpty, lastSelector, currentElements,\n            selectorsMultiplied;\n    \n        for (i = 0; i < selector.elements.length; i++) {\n            el = selector.elements[i];\n            if (el.value === '&') {\n                hasParentSelector = true;\n            }\n        }\n    \n        if (!hasParentSelector) {\n            if (context.length > 0) {\n                for (i = 0; i < context.length; i++) {\n                    paths.push(context[i].concat(selector));\n                }\n            }\n            else {\n                paths.push([selector]);\n            }\n            return;\n        }\n\n        // The paths are [[Selector]]\n        // The first list is a list of comma seperated selectors\n        // The inner list is a list of inheritance seperated selectors\n        // e.g.\n        // .a, .b {\n        //   .c {\n        //   }\n        // }\n        // == [[.a] [.c]] [[.b] [.c]]\n        //\n\n        // the elements from the current selector so far\n        currentElements = [];\n        // the current list of new selectors to add to the path.\n        // We will build it up. We initiate it with one empty selector as we \"multiply\" the new selectors\n        // by the parents\n        newSelectors = [[]];\n\n        for (i = 0; i < selector.elements.length; i++) {\n            el = selector.elements[i];\n            // non parent reference elements just get added\n            if (el.value !== \"&\") {\n                currentElements.push(el);\n            } else {\n                // the new list of selectors to add\n                selectorsMultiplied = [];\n\n                // merge the current list of non parent selector elements\n                // on to the current list of selectors to add\n                if (currentElements.length > 0) {\n                    this.mergeElementsOnToSelectors(currentElements, newSelectors);\n                }\n\n                // loop through our current selectors\n                for (j = 0; j < newSelectors.length; j++) {\n                    sel = newSelectors[j];\n                    // if we don't have any parent paths, the & might be in a mixin so that it can be used\n                    // whether there are parents or not\n                    if (context.length === 0) {\n                        // the combinator used on el should now be applied to the next element instead so that\n                        // it is not lost\n                        if (sel.length > 0) {\n                            sel[0].elements = sel[0].elements.slice(0);\n                            sel[0].elements.push(new(tree.Element)(el.combinator, '', el.index, el.currentFileInfo));\n                        }\n                        selectorsMultiplied.push(sel);\n                    }\n                    else {\n                        // and the parent selectors\n                        for (k = 0; k < context.length; k++) {\n                            parentSel = context[k];\n                            // We need to put the current selectors\n                            // then join the last selector's elements on to the parents selectors\n\n                            // our new selector path\n                            newSelectorPath = [];\n                            // selectors from the parent after the join\n                            afterParentJoin = [];\n                            newJoinedSelectorEmpty = true;\n\n                            //construct the joined selector - if & is the first thing this will be empty,\n                            // if not newJoinedSelector will be the last set of elements in the selector\n                            if (sel.length > 0) {\n                                newSelectorPath = sel.slice(0);\n                                lastSelector = newSelectorPath.pop();\n                                newJoinedSelector = selector.createDerived(lastSelector.elements.slice(0));\n                                newJoinedSelectorEmpty = false;\n                            }\n                            else {\n                                newJoinedSelector = selector.createDerived([]);\n                            }\n\n                            //put together the parent selectors after the join\n                            if (parentSel.length > 1) {\n                                afterParentJoin = afterParentJoin.concat(parentSel.slice(1));\n                            }\n\n                            if (parentSel.length > 0) {\n                                newJoinedSelectorEmpty = false;\n\n                                // join the elements so far with the first part of the parent\n                                newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, el.index, el.currentFileInfo));\n                                newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1));\n                            }\n\n                            if (!newJoinedSelectorEmpty) {\n                                // now add the joined selector\n                                newSelectorPath.push(newJoinedSelector);\n                            }\n\n                            // and the rest of the parent\n                            newSelectorPath = newSelectorPath.concat(afterParentJoin);\n\n                            // add that to our new set of selectors\n                            selectorsMultiplied.push(newSelectorPath);\n                        }\n                    }\n                }\n\n                // our new selectors has been multiplied, so reset the state\n                newSelectors = selectorsMultiplied;\n                currentElements = [];\n            }\n        }\n\n        // if we have any elements left over (e.g. .a& .b == .b)\n        // add them on to all the current selectors\n        if (currentElements.length > 0) {\n            this.mergeElementsOnToSelectors(currentElements, newSelectors);\n        }\n\n        for (i = 0; i < newSelectors.length; i++) {\n            if (newSelectors[i].length > 0) {\n                paths.push(newSelectors[i]);\n            }\n        }\n    },\n    \n    mergeElementsOnToSelectors: function(elements, selectors) {\n        var i, sel;\n\n        if (selectors.length === 0) {\n            selectors.push([ new(tree.Selector)(elements) ]);\n            return;\n        }\n\n        for (i = 0; i < selectors.length; i++) {\n            sel = selectors[i];\n\n            // if the previous thing in sel is a parent this needs to join on to it\n            if (sel.length > 0) {\n                sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements));\n            }\n            else {\n                sel.push(new(tree.Selector)(elements));\n            }\n        }\n    }\n};\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Selector = function (elements, extendList, condition, index, currentFileInfo, isReferenced) {\n    this.elements = elements;\n    this.extendList = extendList;\n    this.condition = condition;\n    this.currentFileInfo = currentFileInfo || {};\n    this.isReferenced = isReferenced;\n    if (!condition) {\n        this.evaldCondition = true;\n    }\n};\ntree.Selector.prototype = {\n    type: \"Selector\",\n    accept: function (visitor) {\n        if (this.elements) {\n            this.elements = visitor.visitArray(this.elements);\n        }\n        if (this.extendList) {\n            this.extendList = visitor.visitArray(this.extendList);\n        }\n        if (this.condition) {\n            this.condition = visitor.visit(this.condition);\n        }\n    },\n    createDerived: function(elements, extendList, evaldCondition) {\n        evaldCondition = (evaldCondition != null) ? evaldCondition : this.evaldCondition;\n        var newSelector = new(tree.Selector)(elements, extendList || this.extendList, null, this.index, this.currentFileInfo, this.isReferenced);\n        newSelector.evaldCondition = evaldCondition;\n        newSelector.mediaEmpty = this.mediaEmpty;\n        return newSelector;\n    },\n    match: function (other) {\n        var elements = this.elements,\n            len = elements.length,\n            olen, i;\n\n        other.CacheElements();\n\n        olen = other._elements.length;\n        if (olen === 0 || len < olen) {\n            return 0;\n        } else {\n            for (i = 0; i < olen; i++) {\n                if (elements[i].value !== other._elements[i]) {\n                    return 0;\n                }\n            }\n        }\n\n        return olen; // return number of matched elements\n    },\n    CacheElements: function(){\n        var css = '', len, v, i;\n\n        if( !this._elements ){\n\n            len = this.elements.length;\n            for(i = 0; i < len; i++){\n\n                v = this.elements[i];\n                css += v.combinator.value;\n\n                if( !v.value.value ){\n                    css += v.value;\n                    continue;\n                }\n\n                if( typeof v.value.value !== \"string\" ){\n                    css = '';\n                    break;\n                }\n                css += v.value.value;\n            }\n\n            this._elements = css.match(/[,&#\\.\\w-]([\\w-]|(\\\\.))*/g);\n\n            if (this._elements) {\n                if (this._elements[0] === \"&\") {\n                    this._elements.shift();\n                }\n\n            } else {\n                this._elements = [];\n            }\n\n        }\n    },\n    isJustParentSelector: function() {\n        return !this.mediaEmpty && \n            this.elements.length === 1 && \n            this.elements[0].value === '&' && \n            (this.elements[0].combinator.value === ' ' || this.elements[0].combinator.value === '');\n    },\n    eval: function (env) {\n        var evaldCondition = this.condition && this.condition.eval(env),\n            elements = this.elements, extendList = this.extendList;\n\n        elements = elements && elements.map(function (e) { return e.eval(env); });\n        extendList = extendList && extendList.map(function(extend) { return extend.eval(env); });\n\n        return this.createDerived(elements, extendList, evaldCondition);\n    },\n    genCSS: function (env, output) {\n        var i, element;\n        if ((!env || !env.firstSelector) && this.elements[0].combinator.value === \"\") {\n            output.add(' ', this.currentFileInfo, this.index);\n        }\n        if (!this._css) {\n            //TODO caching? speed comparison?\n            for(i = 0; i < this.elements.length; i++) {\n                element = this.elements[i];\n                element.genCSS(env, output);\n            }\n        }\n    },\n    toCSS: tree.toCSS,\n    markReferenced: function () {\n        this.isReferenced = true;\n    },\n    getIsReferenced: function() {\n        return !this.currentFileInfo.reference || this.isReferenced;\n    },\n    getIsOutput: function() {\n        return this.evaldCondition;\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.UnicodeDescriptor = function (value) {\n    this.value = value;\n};\ntree.UnicodeDescriptor.prototype = {\n    type: \"UnicodeDescriptor\",\n    genCSS: function (env, output) {\n        output.add(this.value);\n    },\n    toCSS: tree.toCSS,\n    eval: function () { return this; }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.URL = function (val, currentFileInfo, isEvald) {\n    this.value = val;\n    this.currentFileInfo = currentFileInfo;\n    this.isEvald = isEvald;\n};\ntree.URL.prototype = {\n    type: \"Url\",\n    accept: function (visitor) {\n        this.value = visitor.visit(this.value);\n    },\n    genCSS: function (env, output) {\n        output.add(\"url(\");\n        this.value.genCSS(env, output);\n        output.add(\")\");\n    },\n    toCSS: tree.toCSS,\n    eval: function (ctx) {\n        var val = this.value.eval(ctx),\n            rootpath;\n\n        if (!this.isEvald) {\n            // Add the base path if the URL is relative\n            rootpath = this.currentFileInfo && this.currentFileInfo.rootpath;\n            if (rootpath && typeof val.value === \"string\" && ctx.isPathRelative(val.value)) {\n                if (!val.quote) {\n                    rootpath = rootpath.replace(/[\\(\\)'\"\\s]/g, function(match) { return \"\\\\\"+match; });\n                }\n                val.value = rootpath + val.value;\n            }\n            \n            val.value = ctx.normalizePath(val.value);\n\n            // Add url args if enabled\n            if (ctx.urlArgs) {\n                if (!val.value.match(/^\\s*data:/)) {\n                    var delimiter = val.value.indexOf('?') === -1 ? '?' : '&';\n                    var urlArgs = delimiter + ctx.urlArgs;\n                    if (val.value.indexOf('#') !== -1) {\n                        val.value = val.value.replace('#', urlArgs + '#');\n                    } else {\n                        val.value += urlArgs;\n                    }\n                }\n            }\n        }\n\n        return new(tree.URL)(val, this.currentFileInfo, true);\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Value = function (value) {\n    this.value = value;\n};\ntree.Value.prototype = {\n    type: \"Value\",\n    accept: function (visitor) {\n        if (this.value) {\n            this.value = visitor.visitArray(this.value);\n        }\n    },\n    eval: function (env) {\n        if (this.value.length === 1) {\n            return this.value[0].eval(env);\n        } else {\n            return new(tree.Value)(this.value.map(function (v) {\n                return v.eval(env);\n            }));\n        }\n    },\n    genCSS: function (env, output) {\n        var i;\n        for(i = 0; i < this.value.length; i++) {\n            this.value[i].genCSS(env, output);\n            if (i+1 < this.value.length) {\n                output.add((env && env.compress) ? ',' : ', ');\n            }\n        }\n    },\n    toCSS: tree.toCSS\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\ntree.Variable = function (name, index, currentFileInfo) {\n    this.name = name;\n    this.index = index;\n    this.currentFileInfo = currentFileInfo || {};\n};\ntree.Variable.prototype = {\n    type: \"Variable\",\n    eval: function (env) {\n        var variable, name = this.name;\n\n        if (name.indexOf('@@') === 0) {\n            name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value;\n        }\n        \n        if (this.evaluating) {\n            throw { type: 'Name',\n                    message: \"Recursive variable definition for \" + name,\n                    filename: this.currentFileInfo.file,\n                    index: this.index };\n        }\n        \n        this.evaluating = true;\n\n        variable = tree.find(env.frames, function (frame) {\n            var v = frame.variable(name);\n            if (v) {\n                return v.value.eval(env);\n            }\n        });\n        if (variable) { \n            this.evaluating = false;\n            return variable;\n        } else {\n            throw { type: 'Name',\n                    message: \"variable \" + name + \" is undefined\",\n                    filename: this.currentFileInfo.filename,\n                    index: this.index };\n        }\n    }\n};\n\n})(require('../tree'));\n\n(function (tree) {\n\n    var parseCopyProperties = [\n        'paths',            // option - unmodified - paths to search for imports on\n        'optimization',     // option - optimization level (for the chunker)\n        'files',            // list of files that have been imported, used for import-once\n        'contents',         // map - filename to contents of all the files\n        'contentsIgnoredChars', // map - filename to lines at the begining of each file to ignore\n        'relativeUrls',     // option - whether to adjust URL's to be relative\n        'rootpath',         // option - rootpath to append to URL's\n        'strictImports',    // option -\n        'insecure',         // option - whether to allow imports from insecure ssl hosts\n        'dumpLineNumbers',  // option - whether to dump line numbers\n        'compress',         // option - whether to compress\n        'processImports',   // option - whether to process imports. if false then imports will not be imported\n        'syncImport',       // option - whether to import synchronously\n        'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true\n        'mime',             // browser only - mime type for sheet import\n        'useFileCache',     // browser only - whether to use the per file session cache\n        'currentFileInfo'   // information about the current file - for error reporting and importing and making urls relative etc.\n    ];\n\n    //currentFileInfo = {\n    //  'relativeUrls' - option - whether to adjust URL's to be relative\n    //  'filename' - full resolved filename of current file\n    //  'rootpath' - path to append to normal URLs for this node\n    //  'currentDirectory' - path to the current file, absolute\n    //  'rootFilename' - filename of the base file\n    //  'entryPath' - absolute path to the entry file\n    //  'reference' - whether the file should not be output and only output parts that are referenced\n\n    tree.parseEnv = function(options) {\n        copyFromOriginal(options, this, parseCopyProperties);\n\n        if (!this.contents) { this.contents = {}; }\n        if (!this.contentsIgnoredChars) { this.contentsIgnoredChars = {}; }\n        if (!this.files) { this.files = {}; }\n\n        if (!this.currentFileInfo) {\n            var filename = (options && options.filename) || \"input\";\n            var entryPath = filename.replace(/[^\\/\\\\]*$/, \"\");\n            if (options) {\n                options.filename = null;\n            }\n            this.currentFileInfo = {\n                filename: filename,\n                relativeUrls: this.relativeUrls,\n                rootpath: (options && options.rootpath) || \"\",\n                currentDirectory: entryPath,\n                entryPath: entryPath,\n                rootFilename: filename\n            };\n        }\n    };\n\n    var evalCopyProperties = [\n        'silent',         // whether to swallow errors and warnings\n        'verbose',        // whether to log more activity\n        'compress',       // whether to compress\n        'yuicompress',    // whether to compress with the outside tool yui compressor\n        'ieCompat',       // whether to enforce IE compatibility (IE8 data-uri)\n        'strictMath',     // whether math has to be within parenthesis\n        'strictUnits',    // whether units need to evaluate correctly\n        'cleancss',       // whether to compress with clean-css\n        'sourceMap',      // whether to output a source map\n        'importMultiple', // whether we are currently importing multiple copies\n        'urlArgs'         // whether to add args into url tokens\n        ];\n\n    tree.evalEnv = function(options, frames) {\n        copyFromOriginal(options, this, evalCopyProperties);\n\n        this.frames = frames || [];\n    };\n\n    tree.evalEnv.prototype.inParenthesis = function () {\n        if (!this.parensStack) {\n            this.parensStack = [];\n        }\n        this.parensStack.push(true);\n    };\n\n    tree.evalEnv.prototype.outOfParenthesis = function () {\n        this.parensStack.pop();\n    };\n\n    tree.evalEnv.prototype.isMathOn = function () {\n        return this.strictMath ? (this.parensStack && this.parensStack.length) : true;\n    };\n\n    tree.evalEnv.prototype.isPathRelative = function (path) {\n        return !/^(?:[a-z-]+:|\\/)/.test(path);\n    };\n\n    tree.evalEnv.prototype.normalizePath = function( path ) {\n        var\n          segments = path.split(\"/\").reverse(),\n          segment;\n\n        path = [];\n        while (segments.length !== 0 ) {\n            segment = segments.pop();\n            switch( segment ) {\n                case \".\":\n                    break;\n                case \"..\":\n                    if ((path.length === 0) || (path[path.length - 1] === \"..\")) {\n                        path.push( segment );\n                    } else {\n                        path.pop();\n                    }\n                    break;\n                default:\n                    path.push( segment );\n                    break;\n            }\n        }\n\n        return path.join(\"/\");\n    };\n\n    //todo - do the same for the toCSS env\n    //tree.toCSSEnv = function (options) {\n    //};\n\n    var copyFromOriginal = function(original, destination, propertiesToCopy) {\n        if (!original) { return; }\n\n        for(var i = 0; i < propertiesToCopy.length; i++) {\n            if (original.hasOwnProperty(propertiesToCopy[i])) {\n                destination[propertiesToCopy[i]] = original[propertiesToCopy[i]];\n            }\n        }\n    };\n\n})(require('./tree'));\n\n(function (tree) {\n\n    var _visitArgs = { visitDeeper: true },\n        _hasIndexed = false;\n\n    function _noop(node) {\n        return node;\n    }\n\n    function indexNodeTypes(parent, ticker) {\n        // add .typeIndex to tree node types for lookup table\n        var key, child;\n        for (key in parent) {\n            if (parent.hasOwnProperty(key)) {\n                child = parent[key];\n                switch (typeof child) {\n                    case \"function\":\n                        // ignore bound functions directly on tree which do not have a prototype\n                        // or aren't nodes\n                        if (child.prototype && child.prototype.type) {\n                            child.prototype.typeIndex = ticker++;\n                        }\n                        break;\n                    case \"object\":\n                        ticker = indexNodeTypes(child, ticker);\n                        break;\n                }\n            }\n        }\n        return ticker;\n    }\n\n    tree.visitor = function(implementation) {\n        this._implementation = implementation;\n        this._visitFnCache = [];\n\n        if (!_hasIndexed) {\n            indexNodeTypes(tree, 1);\n            _hasIndexed = true;\n        }\n    };\n\n    tree.visitor.prototype = {\n        visit: function(node) {\n            if (!node) {\n                return node;\n            }\n\n            var nodeTypeIndex = node.typeIndex;\n            if (!nodeTypeIndex) {\n                return node;\n            }\n\n            var visitFnCache = this._visitFnCache,\n                impl = this._implementation,\n                aryIndx = nodeTypeIndex << 1,\n                outAryIndex = aryIndx | 1,\n                func = visitFnCache[aryIndx],\n                funcOut = visitFnCache[outAryIndex],\n                visitArgs = _visitArgs,\n                fnName;\n\n            visitArgs.visitDeeper = true;\n\n            if (!func) {\n                fnName = \"visit\" + node.type;\n                func = impl[fnName] || _noop;\n                funcOut = impl[fnName + \"Out\"] || _noop;\n                visitFnCache[aryIndx] = func;\n                visitFnCache[outAryIndex] = funcOut;\n            }\n\n            if (func !== _noop) {\n                var newNode = func.call(impl, node, visitArgs);\n                if (impl.isReplacing) {\n                    node = newNode;\n                }\n            }\n\n            if (visitArgs.visitDeeper && node && node.accept) {\n                node.accept(this);\n            }\n\n            if (funcOut != _noop) {\n                funcOut.call(impl, node);\n            }\n\n            return node;\n        },\n        visitArray: function(nodes, nonReplacing) {\n            if (!nodes) {\n                return nodes;\n            }\n\n            var cnt = nodes.length, i;\n\n            // Non-replacing\n            if (nonReplacing || !this._implementation.isReplacing) {\n                for (i = 0; i < cnt; i++) {\n                    this.visit(nodes[i]);\n                }\n                return nodes;\n            }\n\n            // Replacing\n            var out = [];\n            for (i = 0; i < cnt; i++) {\n                var evald = this.visit(nodes[i]);\n                if (!evald.splice) {\n                    out.push(evald);\n                } else if (evald.length) {\n                    this.flatten(evald, out);\n                }\n            }\n            return out;\n        },\n        flatten: function(arr, out) {\n            if (!out) {\n                out = [];\n            }\n\n            var cnt, i, item,\n                nestedCnt, j, nestedItem;\n\n            for (i = 0, cnt = arr.length; i < cnt; i++) {\n                item = arr[i];\n                if (!item.splice) {\n                    out.push(item);\n                    continue;\n                }\n\n                for (j = 0, nestedCnt = item.length; j < nestedCnt; j++) {\n                    nestedItem = item[j];\n                    if (!nestedItem.splice) {\n                        out.push(nestedItem);\n                    } else if (nestedItem.length) {\n                        this.flatten(nestedItem, out);\n                    }\n                }\n            }\n\n            return out;\n        }\n    };\n\n})(require('./tree'));\n(function (tree) {\n    tree.importVisitor = function(importer, finish, evalEnv, onceFileDetectionMap, recursionDetector) {\n        this._visitor = new tree.visitor(this);\n        this._importer = importer;\n        this._finish = finish;\n        this.env = evalEnv || new tree.evalEnv();\n        this.importCount = 0;\n        this.onceFileDetectionMap = onceFileDetectionMap || {};\n        this.recursionDetector = {};\n        if (recursionDetector) {\n            for(var fullFilename in recursionDetector) {\n                if (recursionDetector.hasOwnProperty(fullFilename)) {\n                    this.recursionDetector[fullFilename] = true;\n                }\n            }\n        }\n    };\n\n    tree.importVisitor.prototype = {\n        isReplacing: true,\n        run: function (root) {\n            var error;\n            try {\n                // process the contents\n                this._visitor.visit(root);\n            }\n            catch(e) {\n                error = e;\n            }\n\n            this.isFinished = true;\n\n            if (this.importCount === 0) {\n                this._finish(error);\n            }\n        },\n        visitImport: function (importNode, visitArgs) {\n            var importVisitor = this,\n                evaldImportNode,\n                inlineCSS = importNode.options.inline;\n            \n            if (!importNode.css || inlineCSS) {\n\n                try {\n                    evaldImportNode = importNode.evalForImport(this.env);\n                } catch(e){\n                    if (!e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; }\n                    // attempt to eval properly and treat as css\n                    importNode.css = true;\n                    // if that fails, this error will be thrown\n                    importNode.error = e;\n                }\n\n                if (evaldImportNode && (!evaldImportNode.css || inlineCSS)) {\n                    importNode = evaldImportNode;\n                    this.importCount++;\n                    var env = new tree.evalEnv(this.env, this.env.frames.slice(0));\n\n                    if (importNode.options.multiple) {\n                        env.importMultiple = true;\n                    }\n\n                    this._importer.push(importNode.getPath(), importNode.currentFileInfo, importNode.options, function (e, root, importedAtRoot, fullPath) {\n                        if (e && !e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; }\n\n                        if (!env.importMultiple) { \n                            if (importedAtRoot) {\n                                importNode.skip = true;\n                            } else {\n                                importNode.skip = function() {\n                                    if (fullPath in importVisitor.onceFileDetectionMap) {\n                                        return true;\n                                    }\n                                    importVisitor.onceFileDetectionMap[fullPath] = true;\n                                    return false;\n                                }; \n                            }\n                        }\n\n                        var subFinish = function(e) {\n                            importVisitor.importCount--;\n\n                            if (importVisitor.importCount === 0 && importVisitor.isFinished) {\n                                importVisitor._finish(e);\n                            }\n                        };\n\n                        if (root) {\n                            importNode.root = root;\n                            importNode.importedFilename = fullPath;\n                            var duplicateImport = importedAtRoot || fullPath in importVisitor.recursionDetector;\n\n                            if (!inlineCSS && (env.importMultiple || !duplicateImport)) {\n                                importVisitor.recursionDetector[fullPath] = true;\n                                new(tree.importVisitor)(importVisitor._importer, subFinish, env, importVisitor.onceFileDetectionMap, importVisitor.recursionDetector)\n                                    .run(root);\n                                return;\n                            }\n                        }\n\n                        subFinish();\n                    });\n                }\n            }\n            visitArgs.visitDeeper = false;\n            return importNode;\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n            return ruleNode;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            this.env.frames.unshift(directiveNode);\n            return directiveNode;\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.env.frames.shift();\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            this.env.frames.unshift(mixinDefinitionNode);\n            return mixinDefinitionNode;\n        },\n        visitMixinDefinitionOut: function (mixinDefinitionNode) {\n            this.env.frames.shift();\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n            this.env.frames.unshift(rulesetNode);\n            return rulesetNode;\n        },\n        visitRulesetOut: function (rulesetNode) {\n            this.env.frames.shift();\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            this.env.frames.unshift(mediaNode.ruleset);\n            return mediaNode;\n        },\n        visitMediaOut: function (mediaNode) {\n            this.env.frames.shift();\n        }\n    };\n\n})(require('./tree'));\n(function (tree) {\n    tree.joinSelectorVisitor = function() {\n        this.contexts = [[]];\n        this._visitor = new tree.visitor(this);\n    };\n\n    tree.joinSelectorVisitor.prototype = {\n        run: function (root) {\n            return this._visitor.visit(root);\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n\n        visitRuleset: function (rulesetNode, visitArgs) {\n            var context = this.contexts[this.contexts.length - 1],\n                paths = [], selectors;\n\n            this.contexts.push(paths);\n\n            if (! rulesetNode.root) {\n                selectors = rulesetNode.selectors;\n                if (selectors) {\n                    selectors = selectors.filter(function(selector) { return selector.getIsOutput(); });\n                    rulesetNode.selectors = selectors.length ? selectors : (selectors = null);\n                    if (selectors) { rulesetNode.joinSelectors(paths, context, selectors); }\n                }\n                if (!selectors) { rulesetNode.rules = null; }\n                rulesetNode.paths = paths;\n            }\n        },\n        visitRulesetOut: function (rulesetNode) {\n            this.contexts.length = this.contexts.length - 1;\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            var context = this.contexts[this.contexts.length - 1];\n            mediaNode.rules[0].root = (context.length === 0 || context[0].multiMedia);\n        }\n    };\n\n})(require('./tree'));\n(function (tree) {\n    tree.toCSSVisitor = function(env) {\n        this._visitor = new tree.visitor(this);\n        this._env = env;\n    };\n\n    tree.toCSSVisitor.prototype = {\n        isReplacing: true,\n        run: function (root) {\n            return this._visitor.visit(root);\n        },\n\n        visitRule: function (ruleNode, visitArgs) {\n            if (ruleNode.variable) {\n                return [];\n            }\n            return ruleNode;\n        },\n\n        visitMixinDefinition: function (mixinNode, visitArgs) {\n            // mixin definitions do not get eval'd - this means they keep state\n            // so we have to clear that state here so it isn't used if toCSS is called twice\n            mixinNode.frames = [];\n            return [];\n        },\n\n        visitExtend: function (extendNode, visitArgs) {\n            return [];\n        },\n\n        visitComment: function (commentNode, visitArgs) {\n            if (commentNode.isSilent(this._env)) {\n                return [];\n            }\n            return commentNode;\n        },\n\n        visitMedia: function(mediaNode, visitArgs) {\n            mediaNode.accept(this._visitor);\n            visitArgs.visitDeeper = false;\n\n            if (!mediaNode.rules.length) {\n                return [];\n            }\n            return mediaNode;\n        },\n\n        visitDirective: function(directiveNode, visitArgs) {\n            if (directiveNode.currentFileInfo.reference && !directiveNode.isReferenced) {\n                return [];\n            }\n            if (directiveNode.name === \"@charset\") {\n                // Only output the debug info together with subsequent @charset definitions\n                // a comment (or @media statement) before the actual @charset directive would\n                // be considered illegal css as it has to be on the first line\n                if (this.charset) {\n                    if (directiveNode.debugInfo) {\n                        var comment = new tree.Comment(\"/* \" + directiveNode.toCSS(this._env).replace(/\\n/g, \"\")+\" */\\n\");\n                        comment.debugInfo = directiveNode.debugInfo;\n                        return this._visitor.visit(comment);\n                    }\n                    return [];\n                }\n                this.charset = true;\n            }\n            return directiveNode;\n        },\n\n        checkPropertiesInRoot: function(rules) {\n            var ruleNode;\n            for(var i = 0; i < rules.length; i++) {\n                ruleNode = rules[i];\n                if (ruleNode instanceof tree.Rule && !ruleNode.variable) {\n                    throw { message: \"properties must be inside selector blocks, they cannot be in the root.\",\n                        index: ruleNode.index, filename: ruleNode.currentFileInfo ? ruleNode.currentFileInfo.filename : null};\n                }\n            }\n        },\n\n        visitRuleset: function (rulesetNode, visitArgs) {\n            var rule, rulesets = [];\n            if (rulesetNode.firstRoot) {\n                this.checkPropertiesInRoot(rulesetNode.rules);\n            }\n            if (! rulesetNode.root) {\n                if (rulesetNode.paths) {\n                    rulesetNode.paths = rulesetNode.paths\n                        .filter(function(p) {\n                            var i;\n                            if (p[0].elements[0].combinator.value === ' ') {\n                                p[0].elements[0].combinator = new(tree.Combinator)('');\n                            }\n                            for(i = 0; i < p.length; i++) {\n                                if (p[i].getIsReferenced() && p[i].getIsOutput()) {\n                                    return true;\n                                }\n                            }\n                            return false;\n                        });\n                }\n\n                // Compile rules and rulesets\n                var nodeRules = rulesetNode.rules, nodeRuleCnt = nodeRules ? nodeRules.length : 0;\n                for (var i = 0; i < nodeRuleCnt; ) {\n                    rule = nodeRules[i];\n                    if (rule && rule.rules) {\n                        // visit because we are moving them out from being a child\n                        rulesets.push(this._visitor.visit(rule));\n                        nodeRules.splice(i, 1);\n                        nodeRuleCnt--;\n                        continue;\n                    }\n                    i++;\n                }\n                // accept the visitor to remove rules and refactor itself\n                // then we can decide now whether we want it or not\n                if (nodeRuleCnt > 0) {\n                    rulesetNode.accept(this._visitor);\n                } else {\n                    rulesetNode.rules = null;\n                }\n                visitArgs.visitDeeper = false;\n\n                nodeRules = rulesetNode.rules;\n                if (nodeRules) {\n                    this._mergeRules(nodeRules);\n                    nodeRules = rulesetNode.rules;\n                }\n                if (nodeRules) {\n                    this._removeDuplicateRules(nodeRules);\n                    nodeRules = rulesetNode.rules;\n                }\n\n                // now decide whether we keep the ruleset\n                if (nodeRules && nodeRules.length > 0 && rulesetNode.paths.length > 0) {\n                    rulesets.splice(0, 0, rulesetNode);\n                }\n            } else {\n                rulesetNode.accept(this._visitor);\n                visitArgs.visitDeeper = false;\n                if (rulesetNode.firstRoot || (rulesetNode.rules && rulesetNode.rules.length > 0)) {\n                    rulesets.splice(0, 0, rulesetNode);\n                }\n            }\n            if (rulesets.length === 1) {\n                return rulesets[0];\n            }\n            return rulesets;\n        },\n\n        _removeDuplicateRules: function(rules) {\n            if (!rules) { return; }\n\n            // remove duplicates\n            var ruleCache = {},\n                ruleList, rule, i;\n\n            for(i = rules.length - 1; i >= 0 ; i--) {\n                rule = rules[i];\n                if (rule instanceof tree.Rule) {\n                    if (!ruleCache[rule.name]) {\n                        ruleCache[rule.name] = rule;\n                    } else {\n                        ruleList = ruleCache[rule.name];\n                        if (ruleList instanceof tree.Rule) {\n                            ruleList = ruleCache[rule.name] = [ruleCache[rule.name].toCSS(this._env)];\n                        }\n                        var ruleCSS = rule.toCSS(this._env);\n                        if (ruleList.indexOf(ruleCSS) !== -1) {\n                            rules.splice(i, 1);\n                        } else {\n                            ruleList.push(ruleCSS);\n                        }\n                    }\n                }\n            }\n        },\n\n        _mergeRules: function (rules) {\n            if (!rules) { return; }\n\n            var groups = {},\n                parts,\n                rule,\n                key;\n\n            for (var i = 0; i < rules.length; i++) {\n                rule = rules[i];\n\n                if ((rule instanceof tree.Rule) && rule.merge) {\n                    key = [rule.name,\n                        rule.important ? \"!\" : \"\"].join(\",\");\n\n                    if (!groups[key]) {\n                        groups[key] = [];\n                    } else {\n                        rules.splice(i--, 1);\n                    }\n\n                    groups[key].push(rule);\n                }\n            }\n\n            Object.keys(groups).map(function (k) {\n\n                function toExpression(values) {\n                    return new (tree.Expression)(values.map(function (p) {\n                        return p.value;\n                    }));\n                }\n\n                function toValue(values) {\n                    return new (tree.Value)(values.map(function (p) {\n                        return p;\n                    }));\n                }\n\n                parts = groups[k];\n\n                if (parts.length > 1) {\n                    rule = parts[0];\n                    var spacedGroups = [];\n                    var lastSpacedGroup = [];\n                    parts.map(function (p) {\n                    if (p.merge===\"+\") {\n                        if (lastSpacedGroup.length > 0) {\n                                spacedGroups.push(toExpression(lastSpacedGroup));\n                            }\n                            lastSpacedGroup = [];\n                        }\n                        lastSpacedGroup.push(p);\n                    });\n                    spacedGroups.push(toExpression(lastSpacedGroup));\n                    rule.value = toValue(spacedGroups);\n                }\n            });\n        }\n    };\n\n})(require('./tree'));\n(function (tree) {\n    /*jshint loopfunc:true */\n\n    tree.extendFinderVisitor = function() {\n        this._visitor = new tree.visitor(this);\n        this.contexts = [];\n        this.allExtendsStack = [[]];\n    };\n\n    tree.extendFinderVisitor.prototype = {\n        run: function (root) {\n            root = this._visitor.visit(root);\n            root.allExtends = this.allExtendsStack[0];\n            return root;\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n            if (rulesetNode.root) {\n                return;\n            }\n\n            var i, j, extend, allSelectorsExtendList = [], extendList;\n\n            // get &:extend(.a); rules which apply to all selectors in this ruleset\n            var rules = rulesetNode.rules, ruleCnt = rules ? rules.length : 0;\n            for(i = 0; i < ruleCnt; i++) {\n                if (rulesetNode.rules[i] instanceof tree.Extend) {\n                    allSelectorsExtendList.push(rules[i]);\n                    rulesetNode.extendOnEveryPath = true;\n                }\n            }\n\n            // now find every selector and apply the extends that apply to all extends\n            // and the ones which apply to an individual extend\n            var paths = rulesetNode.paths;\n            for(i = 0; i < paths.length; i++) {\n                var selectorPath = paths[i],\n                    selector = selectorPath[selectorPath.length - 1],\n                    selExtendList = selector.extendList;\n\n                extendList = selExtendList ? selExtendList.slice(0).concat(allSelectorsExtendList)\n                                           : allSelectorsExtendList;\n\n                if (extendList) {\n                    extendList = extendList.map(function(allSelectorsExtend) {\n                        return allSelectorsExtend.clone();\n                    });\n                }\n\n                for(j = 0; j < extendList.length; j++) {\n                    this.foundExtends = true;\n                    extend = extendList[j];\n                    extend.findSelfSelectors(selectorPath);\n                    extend.ruleset = rulesetNode;\n                    if (j === 0) { extend.firstExtendOnThisSelectorPath = true; }\n                    this.allExtendsStack[this.allExtendsStack.length-1].push(extend);\n                }\n            }\n\n            this.contexts.push(rulesetNode.selectors);\n        },\n        visitRulesetOut: function (rulesetNode) {\n            if (!rulesetNode.root) {\n                this.contexts.length = this.contexts.length - 1;\n            }\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            mediaNode.allExtends = [];\n            this.allExtendsStack.push(mediaNode.allExtends);\n        },\n        visitMediaOut: function (mediaNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            directiveNode.allExtends = [];\n            this.allExtendsStack.push(directiveNode.allExtends);\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        }\n    };\n\n    tree.processExtendsVisitor = function() {\n        this._visitor = new tree.visitor(this);\n    };\n\n    tree.processExtendsVisitor.prototype = {\n        run: function(root) {\n            var extendFinder = new tree.extendFinderVisitor();\n            extendFinder.run(root);\n            if (!extendFinder.foundExtends) { return root; }\n            root.allExtends = root.allExtends.concat(this.doExtendChaining(root.allExtends, root.allExtends));\n            this.allExtendsStack = [root.allExtends];\n            return this._visitor.visit(root);\n        },\n        doExtendChaining: function (extendsList, extendsListTarget, iterationCount) {\n            //\n            // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting\n            // the selector we would do normally, but we are also adding an extend with the same target selector\n            // this means this new extend can then go and alter other extends\n            //\n            // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors\n            // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if\n            // we look at each selector at a time, as is done in visitRuleset\n\n            var extendIndex, targetExtendIndex, matches, extendsToAdd = [], newSelector, extendVisitor = this, selectorPath, extend, targetExtend, newExtend;\n\n            iterationCount = iterationCount || 0;\n\n            //loop through comparing every extend with every target extend.\n            // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place\n            // e.g.  .a:extend(.b) {}  and .b:extend(.c) {} then the first extend extends the second one\n            // and the second is the target.\n            // the seperation into two lists allows us to process a subset of chains with a bigger set, as is the\n            // case when processing media queries\n            for(extendIndex = 0; extendIndex < extendsList.length; extendIndex++){\n                for(targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++){\n\n                    extend = extendsList[extendIndex];\n                    targetExtend = extendsListTarget[targetExtendIndex];\n\n                    // look for circular references\n                    if( extend.parent_ids.indexOf( targetExtend.object_id ) >= 0 ){ continue; }\n\n                    // find a match in the target extends self selector (the bit before :extend)\n                    selectorPath = [targetExtend.selfSelectors[0]];\n                    matches = extendVisitor.findMatch(extend, selectorPath);\n\n                    if (matches.length) {\n\n                        // we found a match, so for each self selector..\n                        extend.selfSelectors.forEach(function(selfSelector) {\n\n                            // process the extend as usual\n                            newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector);\n\n                            // but now we create a new extend from it\n                            newExtend = new(tree.Extend)(targetExtend.selector, targetExtend.option, 0);\n                            newExtend.selfSelectors = newSelector;\n\n                            // add the extend onto the list of extends for that selector\n                            newSelector[newSelector.length-1].extendList = [newExtend];\n\n                            // record that we need to add it.\n                            extendsToAdd.push(newExtend);\n                            newExtend.ruleset = targetExtend.ruleset;\n\n                            //remember its parents for circular references\n                            newExtend.parent_ids = newExtend.parent_ids.concat(targetExtend.parent_ids, extend.parent_ids);\n\n                            // only process the selector once.. if we have :extend(.a,.b) then multiple\n                            // extends will look at the same selector path, so when extending\n                            // we know that any others will be duplicates in terms of what is added to the css\n                            if (targetExtend.firstExtendOnThisSelectorPath) {\n                                newExtend.firstExtendOnThisSelectorPath = true;\n                                targetExtend.ruleset.paths.push(newSelector);\n                            }\n                        });\n                    }\n                }\n            }\n\n            if (extendsToAdd.length) {\n                // try to detect circular references to stop a stack overflow.\n                // may no longer be needed.\n                this.extendChainCount++;\n                if (iterationCount > 100) {\n                    var selectorOne = \"{unable to calculate}\";\n                    var selectorTwo = \"{unable to calculate}\";\n                    try\n                    {\n                        selectorOne = extendsToAdd[0].selfSelectors[0].toCSS();\n                        selectorTwo = extendsToAdd[0].selector.toCSS();\n                    }\n                    catch(e) {}\n                    throw {message: \"extend circular reference detected. One of the circular extends is currently:\"+selectorOne+\":extend(\" + selectorTwo+\")\"};\n                }\n\n                // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e...\n                return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount+1));\n            } else {\n                return extendsToAdd;\n            }\n        },\n        visitRule: function (ruleNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitMixinDefinition: function (mixinDefinitionNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitSelector: function (selectorNode, visitArgs) {\n            visitArgs.visitDeeper = false;\n        },\n        visitRuleset: function (rulesetNode, visitArgs) {\n            if (rulesetNode.root) {\n                return;\n            }\n            var matches, pathIndex, extendIndex, allExtends = this.allExtendsStack[this.allExtendsStack.length-1], selectorsToAdd = [], extendVisitor = this, selectorPath;\n\n            // look at each selector path in the ruleset, find any extend matches and then copy, find and replace\n\n            for(extendIndex = 0; extendIndex < allExtends.length; extendIndex++) {\n                for(pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) {\n                    selectorPath = rulesetNode.paths[pathIndex];\n\n                    // extending extends happens initially, before the main pass\n                    if (rulesetNode.extendOnEveryPath) { continue; }\n                    var extendList = selectorPath[selectorPath.length-1].extendList;\n                    if (extendList && extendList.length) { continue; }\n\n                    matches = this.findMatch(allExtends[extendIndex], selectorPath);\n\n                    if (matches.length) {\n\n                        allExtends[extendIndex].selfSelectors.forEach(function(selfSelector) {\n                            selectorsToAdd.push(extendVisitor.extendSelector(matches, selectorPath, selfSelector));\n                        });\n                    }\n                }\n            }\n            rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd);\n        },\n        findMatch: function (extend, haystackSelectorPath) {\n            //\n            // look through the haystack selector path to try and find the needle - extend.selector\n            // returns an array of selector matches that can then be replaced\n            //\n            var haystackSelectorIndex, hackstackSelector, hackstackElementIndex, haystackElement,\n                targetCombinator, i,\n                extendVisitor = this,\n                needleElements = extend.selector.elements,\n                potentialMatches = [], potentialMatch, matches = [];\n\n            // loop through the haystack elements\n            for(haystackSelectorIndex = 0; haystackSelectorIndex < haystackSelectorPath.length; haystackSelectorIndex++) {\n                hackstackSelector = haystackSelectorPath[haystackSelectorIndex];\n\n                for(hackstackElementIndex = 0; hackstackElementIndex < hackstackSelector.elements.length; hackstackElementIndex++) {\n\n                    haystackElement = hackstackSelector.elements[hackstackElementIndex];\n\n                    // if we allow elements before our match we can add a potential match every time. otherwise only at the first element.\n                    if (extend.allowBefore || (haystackSelectorIndex === 0 && hackstackElementIndex === 0)) {\n                        potentialMatches.push({pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0, initialCombinator: haystackElement.combinator});\n                    }\n\n                    for(i = 0; i < potentialMatches.length; i++) {\n                        potentialMatch = potentialMatches[i];\n\n                        // selectors add \" \" onto the first element. When we use & it joins the selectors together, but if we don't\n                        // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out\n                        // what the resulting combinator will be\n                        targetCombinator = haystackElement.combinator.value;\n                        if (targetCombinator === '' && hackstackElementIndex === 0) {\n                            targetCombinator = ' ';\n                        }\n\n                        // if we don't match, null our match to indicate failure\n                        if (!extendVisitor.isElementValuesEqual(needleElements[potentialMatch.matched].value, haystackElement.value) ||\n                            (potentialMatch.matched > 0 && needleElements[potentialMatch.matched].combinator.value !== targetCombinator)) {\n                            potentialMatch = null;\n                        } else {\n                            potentialMatch.matched++;\n                        }\n\n                        // if we are still valid and have finished, test whether we have elements after and whether these are allowed\n                        if (potentialMatch) {\n                            potentialMatch.finished = potentialMatch.matched === needleElements.length;\n                            if (potentialMatch.finished &&\n                                (!extend.allowAfter && (hackstackElementIndex+1 < hackstackSelector.elements.length || haystackSelectorIndex+1 < haystackSelectorPath.length))) {\n                                potentialMatch = null;\n                            }\n                        }\n                        // if null we remove, if not, we are still valid, so either push as a valid match or continue\n                        if (potentialMatch) {\n                            if (potentialMatch.finished) {\n                                potentialMatch.length = needleElements.length;\n                                potentialMatch.endPathIndex = haystackSelectorIndex;\n                                potentialMatch.endPathElementIndex = hackstackElementIndex + 1; // index after end of match\n                                potentialMatches.length = 0; // we don't allow matches to overlap, so start matching again\n                                matches.push(potentialMatch);\n                            }\n                        } else {\n                            potentialMatches.splice(i, 1);\n                            i--;\n                        }\n                    }\n                }\n            }\n            return matches;\n        },\n        isElementValuesEqual: function(elementValue1, elementValue2) {\n            if (typeof elementValue1 === \"string\" || typeof elementValue2 === \"string\") {\n                return elementValue1 === elementValue2;\n            }\n            if (elementValue1 instanceof tree.Attribute) {\n                if (elementValue1.op !== elementValue2.op || elementValue1.key !== elementValue2.key) {\n                    return false;\n                }\n                if (!elementValue1.value || !elementValue2.value) {\n                    if (elementValue1.value || elementValue2.value) {\n                        return false;\n                    }\n                    return true;\n                }\n                elementValue1 = elementValue1.value.value || elementValue1.value;\n                elementValue2 = elementValue2.value.value || elementValue2.value;\n                return elementValue1 === elementValue2;\n            }\n            elementValue1 = elementValue1.value;\n            elementValue2 = elementValue2.value;\n            if (elementValue1 instanceof tree.Selector) {\n                if (!(elementValue2 instanceof tree.Selector) || elementValue1.elements.length !== elementValue2.elements.length) {\n                    return false;\n                }\n                for(var i = 0; i <elementValue1.elements.length; i++) {\n                    if (elementValue1.elements[i].combinator.value !== elementValue2.elements[i].combinator.value) {\n                        if (i !== 0 || (elementValue1.elements[i].combinator.value || ' ') !== (elementValue2.elements[i].combinator.value || ' ')) {\n                            return false;\n                        }\n                    }\n                    if (!this.isElementValuesEqual(elementValue1.elements[i].value, elementValue2.elements[i].value)) {\n                        return false;\n                    }\n                }\n                return true;\n            }\n            return false;\n        },\n        extendSelector:function (matches, selectorPath, replacementSelector) {\n\n            //for a set of matches, replace each match with the replacement selector\n\n            var currentSelectorPathIndex = 0,\n                currentSelectorPathElementIndex = 0,\n                path = [],\n                matchIndex,\n                selector,\n                firstElement,\n                match,\n                newElements;\n\n            for (matchIndex = 0; matchIndex < matches.length; matchIndex++) {\n                match = matches[matchIndex];\n                selector = selectorPath[match.pathIndex];\n                firstElement = new tree.Element(\n                    match.initialCombinator,\n                    replacementSelector.elements[0].value,\n                    replacementSelector.elements[0].index,\n                    replacementSelector.elements[0].currentFileInfo\n                );\n\n                if (match.pathIndex > currentSelectorPathIndex && currentSelectorPathElementIndex > 0) {\n                    path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));\n                    currentSelectorPathElementIndex = 0;\n                    currentSelectorPathIndex++;\n                }\n\n                newElements = selector.elements\n                    .slice(currentSelectorPathElementIndex, match.index)\n                    .concat([firstElement])\n                    .concat(replacementSelector.elements.slice(1));\n\n                if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) {\n                    path[path.length - 1].elements =\n                        path[path.length - 1].elements.concat(newElements);\n                } else {\n                    path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex));\n\n                    path.push(new tree.Selector(\n                        newElements\n                    ));\n                }\n                currentSelectorPathIndex = match.endPathIndex;\n                currentSelectorPathElementIndex = match.endPathElementIndex;\n                if (currentSelectorPathElementIndex >= selectorPath[currentSelectorPathIndex].elements.length) {\n                    currentSelectorPathElementIndex = 0;\n                    currentSelectorPathIndex++;\n                }\n            }\n\n            if (currentSelectorPathIndex < selectorPath.length && currentSelectorPathElementIndex > 0) {\n                path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex));\n                currentSelectorPathIndex++;\n            }\n\n            path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length));\n\n            return path;\n        },\n        visitRulesetOut: function (rulesetNode) {\n        },\n        visitMedia: function (mediaNode, visitArgs) {\n            var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);\n            newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends));\n            this.allExtendsStack.push(newAllExtends);\n        },\n        visitMediaOut: function (mediaNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        },\n        visitDirective: function (directiveNode, visitArgs) {\n            var newAllExtends = directiveNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);\n            newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, directiveNode.allExtends));\n            this.allExtendsStack.push(newAllExtends);\n        },\n        visitDirectiveOut: function (directiveNode) {\n            this.allExtendsStack.length = this.allExtendsStack.length - 1;\n        }\n    };\n\n})(require('./tree'));\n\n(function (tree) {\n\n    tree.sourceMapOutput = function (options) {\n        this._css = [];\n        this._rootNode = options.rootNode;\n        this._writeSourceMap = options.writeSourceMap;\n        this._contentsMap = options.contentsMap;\n        this._contentsIgnoredCharsMap = options.contentsIgnoredCharsMap;\n        this._sourceMapFilename = options.sourceMapFilename;\n        this._outputFilename = options.outputFilename;\n        this._sourceMapURL = options.sourceMapURL;\n        if (options.sourceMapBasepath) {\n            this._sourceMapBasepath = options.sourceMapBasepath.replace(/\\\\/g, '/');\n        }\n        this._sourceMapRootpath = options.sourceMapRootpath;\n        this._outputSourceFiles = options.outputSourceFiles;\n        this._sourceMapGeneratorConstructor = options.sourceMapGenerator || require(\"source-map\").SourceMapGenerator;\n\n        if (this._sourceMapRootpath && this._sourceMapRootpath.charAt(this._sourceMapRootpath.length-1) !== '/') {\n            this._sourceMapRootpath += '/';\n        }\n\n        this._lineNumber = 0;\n        this._column = 0;\n    };\n\n    tree.sourceMapOutput.prototype.normalizeFilename = function(filename) {\n        filename = filename.replace(/\\\\/g, '/');\n\n        if (this._sourceMapBasepath && filename.indexOf(this._sourceMapBasepath) === 0) {\n            filename = filename.substring(this._sourceMapBasepath.length);\n            if (filename.charAt(0) === '\\\\' || filename.charAt(0) === '/') {\n               filename = filename.substring(1);\n            }\n        }\n        return (this._sourceMapRootpath || \"\") + filename;\n    };\n\n    tree.sourceMapOutput.prototype.add = function(chunk, fileInfo, index, mapLines) {\n\n        //ignore adding empty strings\n        if (!chunk) {\n            return;\n        }\n\n        var lines,\n            sourceLines,\n            columns,\n            sourceColumns,\n            i;\n\n        if (fileInfo) {\n            var inputSource = this._contentsMap[fileInfo.filename];\n            \n            // remove vars/banner added to the top of the file\n            if (this._contentsIgnoredCharsMap[fileInfo.filename]) {\n                // adjust the index\n                index -= this._contentsIgnoredCharsMap[fileInfo.filename];\n                if (index < 0) { index = 0; }\n                // adjust the source\n                inputSource = inputSource.slice(this._contentsIgnoredCharsMap[fileInfo.filename]);\n            }\n            inputSource = inputSource.substring(0, index);\n            sourceLines = inputSource.split(\"\\n\");\n            sourceColumns = sourceLines[sourceLines.length-1];\n        }\n\n        lines = chunk.split(\"\\n\");\n        columns = lines[lines.length-1];\n\n        if (fileInfo) {\n            if (!mapLines) {\n                this._sourceMapGenerator.addMapping({ generated: { line: this._lineNumber + 1, column: this._column},\n                    original: { line: sourceLines.length, column: sourceColumns.length},\n                    source: this.normalizeFilename(fileInfo.filename)});\n            } else {\n                for(i = 0; i < lines.length; i++) {\n                    this._sourceMapGenerator.addMapping({ generated: { line: this._lineNumber + i + 1, column: i === 0 ? this._column : 0},\n                        original: { line: sourceLines.length + i, column: i === 0 ? sourceColumns.length : 0},\n                        source: this.normalizeFilename(fileInfo.filename)});\n                }\n            }\n        }\n\n        if (lines.length === 1) {\n            this._column += columns.length;\n        } else {\n            this._lineNumber += lines.length - 1;\n            this._column = columns.length;\n        }\n\n        this._css.push(chunk);\n    };\n\n    tree.sourceMapOutput.prototype.isEmpty = function() {\n        return this._css.length === 0;\n    };\n\n    tree.sourceMapOutput.prototype.toCSS = function(env) {\n        this._sourceMapGenerator = new this._sourceMapGeneratorConstructor({ file: this._outputFilename, sourceRoot: null });\n\n        if (this._outputSourceFiles) {\n            for(var filename in this._contentsMap) {\n                if (this._contentsMap.hasOwnProperty(filename))\n                {\n                    var source = this._contentsMap[filename];\n                    if (this._contentsIgnoredCharsMap[filename]) {\n                        source = source.slice(this._contentsIgnoredCharsMap[filename]);\n                    }\n                    this._sourceMapGenerator.setSourceContent(this.normalizeFilename(filename), source);\n                }\n            }\n        }\n\n        this._rootNode.genCSS(env, this);\n\n        if (this._css.length > 0) {\n            var sourceMapURL,\n                sourceMapContent = JSON.stringify(this._sourceMapGenerator.toJSON());\n\n            if (this._sourceMapURL) {\n                sourceMapURL = this._sourceMapURL;\n            } else if (this._sourceMapFilename) {\n                sourceMapURL = this.normalizeFilename(this._sourceMapFilename);\n            }\n\n            if (this._writeSourceMap) {\n                this._writeSourceMap(sourceMapContent);\n            } else {\n                sourceMapURL = \"data:application/json,\" + encodeURIComponent(sourceMapContent);\n            }\n\n            if (sourceMapURL) {\n                this._css.push(\"/*# sourceMappingURL=\" + sourceMapURL + \" */\");\n            }\n        }\n\n        return this._css.join('');\n    };\n\n})(require('./tree'));\n\n//\n// browser.js - client-side engine\n//\n/*global less, window, document, XMLHttpRequest, location */\n\nvar isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol);\n\nless.env = less.env || (location.hostname == '127.0.0.1' ||\n                        location.hostname == '0.0.0.0'   ||\n                        location.hostname == 'localhost' ||\n                        (location.port &&\n                          location.port.length > 0)      ||\n                        isFileProtocol                   ? 'development'\n                                                         : 'production');\n\nvar logLevel = {\n    debug: 3,\n    info: 2,\n    errors: 1,\n    none: 0\n};\n\n// The amount of logging in the javascript console.\n// 3 - Debug, information and errors\n// 2 - Information and errors\n// 1 - Errors\n// 0 - None\n// Defaults to 2\nless.logLevel = typeof(less.logLevel) != 'undefined' ? less.logLevel : (less.env === 'development' ?  logLevel.debug : logLevel.errors);\n\n// Load styles asynchronously (default: false)\n//\n// This is set to `false` by default, so that the body\n// doesn't start loading before the stylesheets are parsed.\n// Setting this to `true` can result in flickering.\n//\nless.async = less.async || false;\nless.fileAsync = less.fileAsync || false;\n\n// Interval between watch polls\nless.poll = less.poll || (isFileProtocol ? 1000 : 1500);\n\n//Setup user functions\nif (less.functions) {\n    for(var func in less.functions) {\n        if (less.functions.hasOwnProperty(func)) {\n            less.tree.functions[func] = less.functions[func];\n        }\n   }\n}\n\nvar dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash);\nif (dumpLineNumbers) {\n    less.dumpLineNumbers = dumpLineNumbers[1];\n}\n\nvar typePattern = /^text\\/(x-)?less$/;\nvar cache = null;\nvar fileCache = {};\n\nfunction log(str, level) {\n    if (typeof(console) !== 'undefined' && less.logLevel >= level) {\n        console.log('less: ' + str);\n    }\n}\n\nfunction extractId(href) {\n    return href.replace(/^[a-z-]+:\\/+?[^\\/]+/, '' )  // Remove protocol & domain\n        .replace(/^\\//,                 '' )  // Remove root /\n        .replace(/\\.[a-zA-Z]+$/,        '' )  // Remove simple extension\n        .replace(/[^\\.\\w-]+/g,          '-')  // Replace illegal characters\n        .replace(/\\./g,                 ':'); // Replace dots with colons(for valid id)\n}\n\nfunction errorConsole(e, rootHref) {\n    var template = '{line} {content}';\n    var filename = e.filename || rootHref;\n    var errors = [];\n    var content = (e.type || \"Syntax\") + \"Error: \" + (e.message || 'There is an error in your .less file') +\n        \" in \" + filename + \" \";\n\n    var errorline = function (e, i, classname) {\n        if (e.extract[i] !== undefined) {\n            errors.push(template.replace(/\\{line\\}/, (parseInt(e.line, 10) || 0) + (i - 1))\n                .replace(/\\{class\\}/, classname)\n                .replace(/\\{content\\}/, e.extract[i]));\n        }\n    };\n\n    if (e.extract) {\n        errorline(e, 0, '');\n        errorline(e, 1, 'line');\n        errorline(e, 2, '');\n        content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\\n' +\n            errors.join('\\n');\n    } else if (e.stack) {\n        content += e.stack;\n    }\n    log(content, logLevel.errors);\n}\n\nfunction createCSS(styles, sheet, lastModified) {\n    // Strip the query-string\n    var href = sheet.href || '';\n\n    // If there is no title set, use the filename, minus the extension\n    var id = 'less:' + (sheet.title || extractId(href));\n\n    // If this has already been inserted into the DOM, we may need to replace it\n    var oldCss = document.getElementById(id);\n    var keepOldCss = false;\n\n    // Create a new stylesheet node for insertion or (if necessary) replacement\n    var css = document.createElement('style');\n    css.setAttribute('type', 'text/css');\n    if (sheet.media) {\n        css.setAttribute('media', sheet.media);\n    }\n    css.id = id;\n\n    if (css.styleSheet) { // IE\n        try {\n            css.styleSheet.cssText = styles;\n        } catch (e) {\n            throw new(Error)(\"Couldn't reassign styleSheet.cssText.\");\n        }\n    } else {\n        css.appendChild(document.createTextNode(styles));\n\n        // If new contents match contents of oldCss, don't replace oldCss\n        keepOldCss = (oldCss !== null && oldCss.childNodes.length > 0 && css.childNodes.length > 0 &&\n            oldCss.firstChild.nodeValue === css.firstChild.nodeValue);\n    }\n\n    var head = document.getElementsByTagName('head')[0];\n\n    // If there is no oldCss, just append; otherwise, only append if we need\n    // to replace oldCss with an updated stylesheet\n    if (oldCss === null || keepOldCss === false) {\n        var nextEl = sheet && sheet.nextSibling || null;\n        if (nextEl) {\n            nextEl.parentNode.insertBefore(css, nextEl);\n        } else {\n            head.appendChild(css);\n        }\n    }\n    if (oldCss && keepOldCss === false) {\n        oldCss.parentNode.removeChild(oldCss);\n    }\n\n    // Don't update the local store if the file wasn't modified\n    if (lastModified && cache) {\n        log('saving ' + href + ' to cache.', logLevel.info);\n        try {\n            cache.setItem(href, styles);\n            cache.setItem(href + ':timestamp', lastModified);\n        } catch(e) {\n            //TODO - could do with adding more robust error handling\n            log('failed to save', logLevel.errors);\n        }\n    }\n}\n\nfunction postProcessCSS(styles) {\n    if (less.postProcessor && typeof less.postProcessor === 'function') {\n        styles = less.postProcessor.call(styles, styles) || styles;\n    }\n    return styles;\n}\n\nfunction errorHTML(e, rootHref) {\n    var id = 'less-error-message:' + extractId(rootHref || \"\");\n    var template = '<li><label>{line}</label><pre class=\"{class}\">{content}</pre></li>';\n    var elem = document.createElement('div'), timer, content, errors = [];\n    var filename = e.filename || rootHref;\n    var filenameNoPath = filename.match(/([^\\/]+(\\?.*)?)$/)[1];\n\n    elem.id        = id;\n    elem.className = \"less-error-message\";\n\n    content = '<h3>'  + (e.type || \"Syntax\") + \"Error: \" + (e.message || 'There is an error in your .less file') +\n        '</h3>' + '<p>in <a href=\"' + filename   + '\">' + filenameNoPath + \"</a> \";\n\n    var errorline = function (e, i, classname) {\n        if (e.extract[i] !== undefined) {\n            errors.push(template.replace(/\\{line\\}/, (parseInt(e.line, 10) || 0) + (i - 1))\n                .replace(/\\{class\\}/, classname)\n                .replace(/\\{content\\}/, e.extract[i]));\n        }\n    };\n\n    if (e.extract) {\n        errorline(e, 0, '');\n        errorline(e, 1, 'line');\n        errorline(e, 2, '');\n        content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':</p>' +\n            '<ul>' + errors.join('') + '</ul>';\n    } else if (e.stack) {\n        content += '<br/>' + e.stack.split('\\n').slice(1).join('<br/>');\n    }\n    elem.innerHTML = content;\n\n    // CSS for error messages\n    createCSS([\n        '.less-error-message ul, .less-error-message li {',\n        'list-style-type: none;',\n        'margin-right: 15px;',\n        'padding: 4px 0;',\n        'margin: 0;',\n        '}',\n        '.less-error-message label {',\n        'font-size: 12px;',\n        'margin-right: 15px;',\n        'padding: 4px 0;',\n        'color: #cc7777;',\n        '}',\n        '.less-error-message pre {',\n        'color: #dd6666;',\n        'padding: 4px 0;',\n        'margin: 0;',\n        'display: inline-block;',\n        '}',\n        '.less-error-message pre.line {',\n        'color: #ff0000;',\n        '}',\n        '.less-error-message h3 {',\n        'font-size: 20px;',\n        'font-weight: bold;',\n        'padding: 15px 0 5px 0;',\n        'margin: 0;',\n        '}',\n        '.less-error-message a {',\n        'color: #10a',\n        '}',\n        '.less-error-message .error {',\n        'color: red;',\n        'font-weight: bold;',\n        'padding-bottom: 2px;',\n        'border-bottom: 1px dashed red;',\n        '}'\n    ].join('\\n'), { title: 'error-message' });\n\n    elem.style.cssText = [\n        \"font-family: Arial, sans-serif\",\n        \"border: 1px solid #e00\",\n        \"background-color: #eee\",\n        \"border-radius: 5px\",\n        \"-webkit-border-radius: 5px\",\n        \"-moz-border-radius: 5px\",\n        \"color: #e00\",\n        \"padding: 15px\",\n        \"margin-bottom: 15px\"\n    ].join(';');\n\n    if (less.env == 'development') {\n        timer = setInterval(function () {\n            if (document.body) {\n                if (document.getElementById(id)) {\n                    document.body.replaceChild(elem, document.getElementById(id));\n                } else {\n                    document.body.insertBefore(elem, document.body.firstChild);\n                }\n                clearInterval(timer);\n            }\n        }, 10);\n    }\n}\n\nfunction error(e, rootHref) {\n    if (!less.errorReporting || less.errorReporting === \"html\") {\n        errorHTML(e, rootHref);\n    } else if (less.errorReporting === \"console\") {\n        errorConsole(e, rootHref);\n    } else if (typeof less.errorReporting === 'function') {\n        less.errorReporting(\"add\", e, rootHref);\n    }\n}\n\nfunction removeErrorHTML(path) {\n    var node = document.getElementById('less-error-message:' + extractId(path));\n    if (node) {\n        node.parentNode.removeChild(node);\n    }\n}\n\nfunction removeErrorConsole(path) {\n    //no action\n}\n\nfunction removeError(path) {\n    if (!less.errorReporting || less.errorReporting === \"html\") {\n        removeErrorHTML(path);\n    } else if (less.errorReporting === \"console\") {\n        removeErrorConsole(path);\n    } else if (typeof less.errorReporting === 'function') {\n        less.errorReporting(\"remove\", path);\n    }\n}\n\nfunction loadStyles(modifyVars) {\n    var styles = document.getElementsByTagName('style'),\n        style;\n    for (var i = 0; i < styles.length; i++) {\n        style = styles[i];\n        if (style.type.match(typePattern)) {\n            var env = new less.tree.parseEnv(less),\n                lessText = style.innerHTML || '';\n            env.filename = document.location.href.replace(/#.*$/, '');\n\n            if (modifyVars || less.globalVars) {\n                env.useFileCache = true;\n            }\n\n            /*jshint loopfunc:true */\n            // use closure to store current value of i\n            var callback = (function(style) {\n                return function (e, cssAST) {\n                    if (e) {\n                        return error(e, \"inline\");\n                    }\n                    var css = cssAST.toCSS(less);\n                    style.type = 'text/css';\n                    if (style.styleSheet) {\n                        style.styleSheet.cssText = css;\n                    } else {\n                        style.innerHTML = css;\n                    }\n                };\n            })(style);\n            new(less.Parser)(env).parse(lessText, callback, {globalVars: less.globalVars, modifyVars: modifyVars});\n        }\n    }\n}\n\nfunction extractUrlParts(url, baseUrl) {\n    // urlParts[1] = protocol&hostname || /\n    // urlParts[2] = / if path relative to host base\n    // urlParts[3] = directories\n    // urlParts[4] = filename\n    // urlParts[5] = parameters\n\n    var urlPartsRegex = /^((?:[a-z-]+:)?\\/+?(?:[^\\/\\?#]*\\/)|([\\/\\\\]))?((?:[^\\/\\\\\\?#]*[\\/\\\\])*)([^\\/\\\\\\?#]*)([#\\?].*)?$/i,\n        urlParts = url.match(urlPartsRegex),\n        returner = {}, directories = [], i, baseUrlParts;\n\n    if (!urlParts) {\n        throw new Error(\"Could not parse sheet href - '\"+url+\"'\");\n    }\n\n    // Stylesheets in IE don't always return the full path\n    if (!urlParts[1] || urlParts[2]) {\n        baseUrlParts = baseUrl.match(urlPartsRegex);\n        if (!baseUrlParts) {\n            throw new Error(\"Could not parse page url - '\"+baseUrl+\"'\");\n        }\n        urlParts[1] = urlParts[1] || baseUrlParts[1] || \"\";\n        if (!urlParts[2]) {\n            urlParts[3] = baseUrlParts[3] + urlParts[3];\n        }\n    }\n\n    if (urlParts[3]) {\n        directories = urlParts[3].replace(/\\\\/g, \"/\").split(\"/\");\n\n        // extract out . before .. so .. doesn't absorb a non-directory\n        for(i = 0; i < directories.length; i++) {\n            if (directories[i] === \".\") {\n                directories.splice(i, 1);\n                i -= 1;\n            }\n        }\n\n        for(i = 0; i < directories.length; i++) {\n            if (directories[i] === \"..\" && i > 0) {\n                directories.splice(i-1, 2);\n                i -= 2;\n            }\n        }\n    }\n\n    returner.hostPart = urlParts[1];\n    returner.directories = directories;\n    returner.path = urlParts[1] + directories.join(\"/\");\n    returner.fileUrl = returner.path + (urlParts[4] || \"\");\n    returner.url = returner.fileUrl + (urlParts[5] || \"\");\n    return returner;\n}\n\nfunction pathDiff(url, baseUrl) {\n    // diff between two paths to create a relative path\n\n    var urlParts = extractUrlParts(url),\n        baseUrlParts = extractUrlParts(baseUrl),\n        i, max, urlDirectories, baseUrlDirectories, diff = \"\";\n    if (urlParts.hostPart !== baseUrlParts.hostPart) {\n        return \"\";\n    }\n    max = Math.max(baseUrlParts.directories.length, urlParts.directories.length);\n    for(i = 0; i < max; i++) {\n        if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; }\n    }\n    baseUrlDirectories = baseUrlParts.directories.slice(i);\n    urlDirectories = urlParts.directories.slice(i);\n    for(i = 0; i < baseUrlDirectories.length-1; i++) {\n        diff += \"../\";\n    }\n    for(i = 0; i < urlDirectories.length-1; i++) {\n        diff += urlDirectories[i] + \"/\";\n    }\n    return diff;\n}\n\nfunction getXMLHttpRequest() {\n    if (window.XMLHttpRequest && (window.location.protocol !== \"file:\" || !window.ActiveXObject)) {\n        return new XMLHttpRequest();\n    } else {\n        try {\n            /*global ActiveXObject */\n            return new ActiveXObject(\"Microsoft.XMLHTTP\");\n        } catch (e) {\n            log(\"browser doesn't support AJAX.\", logLevel.errors);\n            return null;\n        }\n    }\n}\n\nfunction doXHR(url, type, callback, errback) {\n    var xhr = getXMLHttpRequest();\n    var async = isFileProtocol ? less.fileAsync : less.async;\n\n    if (typeof(xhr.overrideMimeType) === 'function') {\n        xhr.overrideMimeType('text/css');\n    }\n    log(\"XHR: Getting '\" + url + \"'\", logLevel.debug);\n    xhr.open('GET', url, async);\n    xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5');\n    xhr.send(null);\n\n    function handleResponse(xhr, callback, errback) {\n        if (xhr.status >= 200 && xhr.status < 300) {\n            callback(xhr.responseText,\n                xhr.getResponseHeader(\"Last-Modified\"));\n        } else if (typeof(errback) === 'function') {\n            errback(xhr.status, url);\n        }\n    }\n\n    if (isFileProtocol && !less.fileAsync) {\n        if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) {\n            callback(xhr.responseText);\n        } else {\n            errback(xhr.status, url);\n        }\n    } else if (async) {\n        xhr.onreadystatechange = function () {\n            if (xhr.readyState == 4) {\n                handleResponse(xhr, callback, errback);\n            }\n        };\n    } else {\n        handleResponse(xhr, callback, errback);\n    }\n}\n\nfunction loadFile(originalHref, currentFileInfo, callback, env, modifyVars) {\n\n    if (currentFileInfo && currentFileInfo.currentDirectory && !/^([a-z-]+:)?\\//.test(originalHref)) {\n        originalHref = currentFileInfo.currentDirectory + originalHref;\n    }\n\n    // sheet may be set to the stylesheet for the initial load or a collection of properties including\n    // some env variables for imports\n    var hrefParts = extractUrlParts(originalHref, window.location.href);\n    var href      = hrefParts.url;\n    var newFileInfo = {\n        currentDirectory: hrefParts.path,\n        filename: href\n    };\n\n    if (currentFileInfo) {\n        newFileInfo.entryPath = currentFileInfo.entryPath;\n        newFileInfo.rootpath = currentFileInfo.rootpath;\n        newFileInfo.rootFilename = currentFileInfo.rootFilename;\n        newFileInfo.relativeUrls = currentFileInfo.relativeUrls;\n    } else {\n        newFileInfo.entryPath = hrefParts.path;\n        newFileInfo.rootpath = less.rootpath || hrefParts.path;\n        newFileInfo.rootFilename = href;\n        newFileInfo.relativeUrls = env.relativeUrls;\n    }\n\n    if (newFileInfo.relativeUrls) {\n        if (env.rootpath) {\n            newFileInfo.rootpath = extractUrlParts(env.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path;\n        } else {\n            newFileInfo.rootpath = hrefParts.path;\n        }\n    }\n\n    if (env.useFileCache && fileCache[href]) {\n        try {\n            var lessText = fileCache[href];\n            callback(null, lessText, href, newFileInfo, { lastModified: new Date() });\n        } catch (e) {\n            callback(e, null, href);\n        }\n        return;\n    }\n\n    doXHR(href, env.mime, function (data, lastModified) {\n        // per file cache\n        fileCache[href] = data;\n\n        // Use remote copy (re-parse)\n        try {\n            callback(null, data, href, newFileInfo, { lastModified: lastModified });\n        } catch (e) {\n            callback(e, null, href);\n        }\n    }, function (status, url) {\n        callback({ type: 'File', message: \"'\" + url + \"' wasn't found (\" + status + \")\" }, null, href);\n    });\n}\n\nfunction loadStyleSheet(sheet, callback, reload, remaining, modifyVars) {\n\n    var env = new less.tree.parseEnv(less);\n    env.mime = sheet.type;\n\n    if (modifyVars || less.globalVars) {\n        env.useFileCache = true;\n    }\n\n    loadFile(sheet.href, null, function(e, data, path, newFileInfo, webInfo) {\n\n        if (webInfo) {\n            webInfo.remaining = remaining;\n\n            var css       = cache && cache.getItem(path),\n                timestamp = cache && cache.getItem(path + ':timestamp');\n\n            if (!reload && timestamp && webInfo.lastModified &&\n                (new(Date)(webInfo.lastModified).valueOf() ===\n                    new(Date)(timestamp).valueOf())) {\n                // Use local copy\n                createCSS(css, sheet);\n                webInfo.local = true;\n                callback(null, null, data, sheet, webInfo, path);\n                return;\n            }\n        }\n\n        //TODO add tests around how this behaves when reloading\n        removeError(path);\n\n        if (data) {\n            env.currentFileInfo = newFileInfo;\n            new(less.Parser)(env).parse(data, function (e, root) {\n                if (e) { return callback(e, null, null, sheet); }\n                try {\n                    callback(e, root, data, sheet, webInfo, path);\n                } catch (e) {\n                    callback(e, null, null, sheet);\n                }\n            }, {modifyVars: modifyVars, globalVars: less.globalVars});\n        } else {\n            callback(e, null, null, sheet, webInfo, path);\n        }\n    }, env, modifyVars);\n}\n\nfunction loadStyleSheets(callback, reload, modifyVars) {\n    for (var i = 0; i < less.sheets.length; i++) {\n        loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1), modifyVars);\n    }\n}\n\nfunction initRunningMode(){\n    if (less.env === 'development') {\n        less.optimization = 0;\n        less.watchTimer = setInterval(function () {\n            if (less.watchMode) {\n                loadStyleSheets(function (e, root, _, sheet, env) {\n                    if (e) {\n                        error(e, sheet.href);\n                    } else if (root) {\n                        var styles = root.toCSS(less);\n                        styles = postProcessCSS(styles);\n                        createCSS(styles, sheet, env.lastModified);\n                    }\n                });\n            }\n        }, less.poll);\n    } else {\n        less.optimization = 3;\n    }\n}\n\n\n\n//\n// Watch mode\n//\nless.watch   = function () {\n    if (!less.watchMode ){\n        less.env = 'development';\n         initRunningMode();\n    }\n    this.watchMode = true;\n    return true;\n};\n\nless.unwatch = function () {clearInterval(less.watchTimer); this.watchMode = false; return false; };\n\nif (/!watch/.test(location.hash)) {\n    less.watch();\n}\n\nif (less.env != 'development') {\n    try {\n        cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage;\n    } catch (_) {}\n}\n\n//\n// Get all <link> tags with the 'rel' attribute set to \"stylesheet/less\"\n//\nvar links = document.getElementsByTagName('link');\n\nless.sheets = [];\n\nfor (var i = 0; i < links.length; i++) {\n    if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) &&\n       (links[i].type.match(typePattern)))) {\n        less.sheets.push(links[i]);\n    }\n}\n\n//\n// With this function, it's possible to alter variables and re-render\n// CSS without reloading less-files\n//\nless.modifyVars = function(record) {\n    less.refresh(false, record);\n};\n\nless.refresh = function (reload, modifyVars) {\n    var startTime, endTime;\n    startTime = endTime = new Date();\n\n    loadStyleSheets(function (e, root, _, sheet, env) {\n        if (e) {\n            return error(e, sheet.href);\n        }\n        if (env.local) {\n            log(\"loading \" + sheet.href + \" from cache.\", logLevel.info);\n        } else {\n            log(\"parsed \" + sheet.href + \" successfully.\", logLevel.debug);\n            var styles = root.toCSS(less);\n            styles = postProcessCSS(styles);\n            createCSS(styles, sheet, env.lastModified);\n        }\n        log(\"css for \" + sheet.href + \" generated in \" + (new Date() - endTime) + 'ms', logLevel.info);\n        if (env.remaining === 0) {\n            log(\"less has finished. css generated in \" + (new Date() - startTime) + 'ms', logLevel.info);\n        }\n        endTime = new Date();\n    }, reload, modifyVars);\n\n    loadStyles(modifyVars);\n};\n\nless.refreshStyles = loadStyles;\n\nless.Parser.fileLoader = loadFile;\n\nless.refresh(less.env === 'development');\n\n// amd.js\n//\n// Define Less as an AMD module.\nif (typeof define === \"function\" && define.amd) {\n    define(function () { return less; } );\n}\n\n})(window);"
  },
  {
    "path": "commons-website/src/main/resources/static/website/vue/vue-2.5.10.js",
    "content": "/*!\n * Vue.js v2.5.10\n * (c) 2014-2017 Evan You\n * Released under the MIT License.\n */\n(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.Vue = factory());\n}(this, (function () { 'use strict';\n\n/*  */\n\nvar emptyObject = Object.freeze({});\n\n// these helpers produces better vm code in JS engines due to their\n// explicitness and function inlining\nfunction isUndef (v) {\n  return v === undefined || v === null\n}\n\nfunction isDef (v) {\n  return v !== undefined && v !== null\n}\n\nfunction isTrue (v) {\n  return v === true\n}\n\nfunction isFalse (v) {\n  return v === false\n}\n\n/**\n * Check if value is primitive\n */\nfunction isPrimitive (value) {\n  return (\n    typeof value === 'string' ||\n    typeof value === 'number' ||\n    typeof value === 'boolean'\n  )\n}\n\n/**\n * Quick object check - this is primarily used to tell\n * Objects from primitive values when we know the value\n * is a JSON-compliant type.\n */\nfunction isObject (obj) {\n  return obj !== null && typeof obj === 'object'\n}\n\n/**\n * Get the raw type string of a value e.g. [object Object]\n */\nvar _toString = Object.prototype.toString;\n\nfunction toRawType (value) {\n  return _toString.call(value).slice(8, -1)\n}\n\n/**\n * Strict object type check. Only returns true\n * for plain JavaScript objects.\n */\nfunction isPlainObject (obj) {\n  return _toString.call(obj) === '[object Object]'\n}\n\nfunction isRegExp (v) {\n  return _toString.call(v) === '[object RegExp]'\n}\n\n/**\n * Check if val is a valid array index.\n */\nfunction isValidArrayIndex (val) {\n  var n = parseFloat(String(val));\n  return n >= 0 && Math.floor(n) === n && isFinite(val)\n}\n\n/**\n * Convert a value to a string that is actually rendered.\n */\nfunction toString (val) {\n  return val == null\n    ? ''\n    : typeof val === 'object'\n      ? JSON.stringify(val, null, 2)\n      : String(val)\n}\n\n/**\n * Convert a input value to a number for persistence.\n * If the conversion fails, return original string.\n */\nfunction toNumber (val) {\n  var n = parseFloat(val);\n  return isNaN(n) ? val : n\n}\n\n/**\n * Make a map and return a function for checking if a key\n * is in that map.\n */\nfunction makeMap (\n  str,\n  expectsLowerCase\n) {\n  var map = Object.create(null);\n  var list = str.split(',');\n  for (var i = 0; i < list.length; i++) {\n    map[list[i]] = true;\n  }\n  return expectsLowerCase\n    ? function (val) { return map[val.toLowerCase()]; }\n    : function (val) { return map[val]; }\n}\n\n/**\n * Check if a tag is a built-in tag.\n */\nvar isBuiltInTag = makeMap('slot,component', true);\n\n/**\n * Check if a attribute is a reserved attribute.\n */\nvar isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');\n\n/**\n * Remove an item from an array\n */\nfunction remove (arr, item) {\n  if (arr.length) {\n    var index = arr.indexOf(item);\n    if (index > -1) {\n      return arr.splice(index, 1)\n    }\n  }\n}\n\n/**\n * Check whether the object has the property.\n */\nvar hasOwnProperty = Object.prototype.hasOwnProperty;\nfunction hasOwn (obj, key) {\n  return hasOwnProperty.call(obj, key)\n}\n\n/**\n * Create a cached version of a pure function.\n */\nfunction cached (fn) {\n  var cache = Object.create(null);\n  return (function cachedFn (str) {\n    var hit = cache[str];\n    return hit || (cache[str] = fn(str))\n  })\n}\n\n/**\n * Camelize a hyphen-delimited string.\n */\nvar camelizeRE = /-(\\w)/g;\nvar camelize = cached(function (str) {\n  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })\n});\n\n/**\n * Capitalize a string.\n */\nvar capitalize = cached(function (str) {\n  return str.charAt(0).toUpperCase() + str.slice(1)\n});\n\n/**\n * Hyphenate a camelCase string.\n */\nvar hyphenateRE = /\\B([A-Z])/g;\nvar hyphenate = cached(function (str) {\n  return str.replace(hyphenateRE, '-$1').toLowerCase()\n});\n\n/**\n * Simple bind, faster than native\n */\nfunction bind (fn, ctx) {\n  function boundFn (a) {\n    var l = arguments.length;\n    return l\n      ? l > 1\n        ? fn.apply(ctx, arguments)\n        : fn.call(ctx, a)\n      : fn.call(ctx)\n  }\n  // record original fn length\n  boundFn._length = fn.length;\n  return boundFn\n}\n\n/**\n * Convert an Array-like object to a real Array.\n */\nfunction toArray (list, start) {\n  start = start || 0;\n  var i = list.length - start;\n  var ret = new Array(i);\n  while (i--) {\n    ret[i] = list[i + start];\n  }\n  return ret\n}\n\n/**\n * Mix properties into target object.\n */\nfunction extend (to, _from) {\n  for (var key in _from) {\n    to[key] = _from[key];\n  }\n  return to\n}\n\n/**\n * Merge an Array of Objects into a single Object.\n */\nfunction toObject (arr) {\n  var res = {};\n  for (var i = 0; i < arr.length; i++) {\n    if (arr[i]) {\n      extend(res, arr[i]);\n    }\n  }\n  return res\n}\n\n/**\n * Perform no operation.\n * Stubbing args to make Flow happy without leaving useless transpiled code\n * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/)\n */\nfunction noop (a, b, c) {}\n\n/**\n * Always return false.\n */\nvar no = function (a, b, c) { return false; };\n\n/**\n * Return same value\n */\nvar identity = function (_) { return _; };\n\n/**\n * Generate a static keys string from compiler modules.\n */\nfunction genStaticKeys (modules) {\n  return modules.reduce(function (keys, m) {\n    return keys.concat(m.staticKeys || [])\n  }, []).join(',')\n}\n\n/**\n * Check if two values are loosely equal - that is,\n * if they are plain objects, do they have the same shape?\n */\nfunction looseEqual (a, b) {\n  if (a === b) { return true }\n  var isObjectA = isObject(a);\n  var isObjectB = isObject(b);\n  if (isObjectA && isObjectB) {\n    try {\n      var isArrayA = Array.isArray(a);\n      var isArrayB = Array.isArray(b);\n      if (isArrayA && isArrayB) {\n        return a.length === b.length && a.every(function (e, i) {\n          return looseEqual(e, b[i])\n        })\n      } else if (!isArrayA && !isArrayB) {\n        var keysA = Object.keys(a);\n        var keysB = Object.keys(b);\n        return keysA.length === keysB.length && keysA.every(function (key) {\n          return looseEqual(a[key], b[key])\n        })\n      } else {\n        /* istanbul ignore next */\n        return false\n      }\n    } catch (e) {\n      /* istanbul ignore next */\n      return false\n    }\n  } else if (!isObjectA && !isObjectB) {\n    return String(a) === String(b)\n  } else {\n    return false\n  }\n}\n\nfunction looseIndexOf (arr, val) {\n  for (var i = 0; i < arr.length; i++) {\n    if (looseEqual(arr[i], val)) { return i }\n  }\n  return -1\n}\n\n/**\n * Ensure a function is called only once.\n */\nfunction once (fn) {\n  var called = false;\n  return function () {\n    if (!called) {\n      called = true;\n      fn.apply(this, arguments);\n    }\n  }\n}\n\nvar SSR_ATTR = 'data-server-rendered';\n\nvar ASSET_TYPES = [\n  'component',\n  'directive',\n  'filter'\n];\n\nvar LIFECYCLE_HOOKS = [\n  'beforeCreate',\n  'created',\n  'beforeMount',\n  'mounted',\n  'beforeUpdate',\n  'updated',\n  'beforeDestroy',\n  'destroyed',\n  'activated',\n  'deactivated',\n  'errorCaptured'\n];\n\n/*  */\n\nvar config = ({\n  /**\n   * Option merge strategies (used in core/util/options)\n   */\n  optionMergeStrategies: Object.create(null),\n\n  /**\n   * Whether to suppress warnings.\n   */\n  silent: false,\n\n  /**\n   * Show production mode tip message on boot?\n   */\n  productionTip: \"development\" !== 'production',\n\n  /**\n   * Whether to enable devtools\n   */\n  devtools: \"development\" !== 'production',\n\n  /**\n   * Whether to record perf\n   */\n  performance: false,\n\n  /**\n   * Error handler for watcher errors\n   */\n  errorHandler: null,\n\n  /**\n   * Warn handler for watcher warns\n   */\n  warnHandler: null,\n\n  /**\n   * Ignore certain custom elements\n   */\n  ignoredElements: [],\n\n  /**\n   * Custom user key aliases for v-on\n   */\n  keyCodes: Object.create(null),\n\n  /**\n   * Check if a tag is reserved so that it cannot be registered as a\n   * component. This is platform-dependent and may be overwritten.\n   */\n  isReservedTag: no,\n\n  /**\n   * Check if an attribute is reserved so that it cannot be used as a component\n   * prop. This is platform-dependent and may be overwritten.\n   */\n  isReservedAttr: no,\n\n  /**\n   * Check if a tag is an unknown element.\n   * Platform-dependent.\n   */\n  isUnknownElement: no,\n\n  /**\n   * Get the namespace of an element\n   */\n  getTagNamespace: noop,\n\n  /**\n   * Parse the real tag name for the specific platform.\n   */\n  parsePlatformTagName: identity,\n\n  /**\n   * Check if an attribute must be bound using property, e.g. value\n   * Platform-dependent.\n   */\n  mustUseProp: no,\n\n  /**\n   * Exposed for legacy reasons\n   */\n  _lifecycleHooks: LIFECYCLE_HOOKS\n});\n\n/*  */\n\n/**\n * Check if a string starts with $ or _\n */\nfunction isReserved (str) {\n  var c = (str + '').charCodeAt(0);\n  return c === 0x24 || c === 0x5F\n}\n\n/**\n * Define a property.\n */\nfunction def (obj, key, val, enumerable) {\n  Object.defineProperty(obj, key, {\n    value: val,\n    enumerable: !!enumerable,\n    writable: true,\n    configurable: true\n  });\n}\n\n/**\n * Parse simple path.\n */\nvar bailRE = /[^\\w.$]/;\nfunction parsePath (path) {\n  if (bailRE.test(path)) {\n    return\n  }\n  var segments = path.split('.');\n  return function (obj) {\n    for (var i = 0; i < segments.length; i++) {\n      if (!obj) { return }\n      obj = obj[segments[i]];\n    }\n    return obj\n  }\n}\n\n/*  */\n\n\n// can we use __proto__?\nvar hasProto = '__proto__' in {};\n\n// Browser environment sniffing\nvar inBrowser = typeof window !== 'undefined';\nvar inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform;\nvar weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();\nvar UA = inBrowser && window.navigator.userAgent.toLowerCase();\nvar isIE = UA && /msie|trident/.test(UA);\nvar isIE9 = UA && UA.indexOf('msie 9.0') > 0;\nvar isEdge = UA && UA.indexOf('edge/') > 0;\nvar isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android');\nvar isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios');\nvar isChrome = UA && /chrome\\/\\d+/.test(UA) && !isEdge;\n\n// Firefox has a \"watch\" function on Object.prototype...\nvar nativeWatch = ({}).watch;\n\nvar supportsPassive = false;\nif (inBrowser) {\n  try {\n    var opts = {};\n    Object.defineProperty(opts, 'passive', ({\n      get: function get () {\n        /* istanbul ignore next */\n        supportsPassive = true;\n      }\n    })); // https://github.com/facebook/flow/issues/285\n    window.addEventListener('test-passive', null, opts);\n  } catch (e) {}\n}\n\n// this needs to be lazy-evaled because vue may be required before\n// vue-server-renderer can set VUE_ENV\nvar _isServer;\nvar isServerRendering = function () {\n  if (_isServer === undefined) {\n    /* istanbul ignore if */\n    if (!inBrowser && typeof global !== 'undefined') {\n      // detect presence of vue-server-renderer and avoid\n      // Webpack shimming the process\n      _isServer = global['process'].env.VUE_ENV === 'server';\n    } else {\n      _isServer = false;\n    }\n  }\n  return _isServer\n};\n\n// detect devtools\nvar devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;\n\n/* istanbul ignore next */\nfunction isNative (Ctor) {\n  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())\n}\n\nvar hasSymbol =\n  typeof Symbol !== 'undefined' && isNative(Symbol) &&\n  typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys);\n\nvar _Set;\n/* istanbul ignore if */ // $flow-disable-line\nif (typeof Set !== 'undefined' && isNative(Set)) {\n  // use native Set when available.\n  _Set = Set;\n} else {\n  // a non-standard Set polyfill that only works with primitive keys.\n  _Set = (function () {\n    function Set () {\n      this.set = Object.create(null);\n    }\n    Set.prototype.has = function has (key) {\n      return this.set[key] === true\n    };\n    Set.prototype.add = function add (key) {\n      this.set[key] = true;\n    };\n    Set.prototype.clear = function clear () {\n      this.set = Object.create(null);\n    };\n\n    return Set;\n  }());\n}\n\n/*  */\n\nvar warn = noop;\nvar tip = noop;\nvar generateComponentTrace = (noop); // work around flow check\nvar formatComponentName = (noop);\n\n{\n  var hasConsole = typeof console !== 'undefined';\n  var classifyRE = /(?:^|[-_])(\\w)/g;\n  var classify = function (str) { return str\n    .replace(classifyRE, function (c) { return c.toUpperCase(); })\n    .replace(/[-_]/g, ''); };\n\n  warn = function (msg, vm) {\n    var trace = vm ? generateComponentTrace(vm) : '';\n\n    if (config.warnHandler) {\n      config.warnHandler.call(null, msg, vm, trace);\n    } else if (hasConsole && (!config.silent)) {\n      console.error((\"[Vue warn]: \" + msg + trace));\n    }\n  };\n\n  tip = function (msg, vm) {\n    if (hasConsole && (!config.silent)) {\n      console.warn(\"[Vue tip]: \" + msg + (\n        vm ? generateComponentTrace(vm) : ''\n      ));\n    }\n  };\n\n  formatComponentName = function (vm, includeFile) {\n    if (vm.$root === vm) {\n      return '<Root>'\n    }\n    var options = typeof vm === 'function' && vm.cid != null\n      ? vm.options\n      : vm._isVue\n        ? vm.$options || vm.constructor.options\n        : vm || {};\n    var name = options.name || options._componentTag;\n    var file = options.__file;\n    if (!name && file) {\n      var match = file.match(/([^/\\\\]+)\\.vue$/);\n      name = match && match[1];\n    }\n\n    return (\n      (name ? (\"<\" + (classify(name)) + \">\") : \"<Anonymous>\") +\n      (file && includeFile !== false ? (\" at \" + file) : '')\n    )\n  };\n\n  var repeat = function (str, n) {\n    var res = '';\n    while (n) {\n      if (n % 2 === 1) { res += str; }\n      if (n > 1) { str += str; }\n      n >>= 1;\n    }\n    return res\n  };\n\n  generateComponentTrace = function (vm) {\n    if (vm._isVue && vm.$parent) {\n      var tree = [];\n      var currentRecursiveSequence = 0;\n      while (vm) {\n        if (tree.length > 0) {\n          var last = tree[tree.length - 1];\n          if (last.constructor === vm.constructor) {\n            currentRecursiveSequence++;\n            vm = vm.$parent;\n            continue\n          } else if (currentRecursiveSequence > 0) {\n            tree[tree.length - 1] = [last, currentRecursiveSequence];\n            currentRecursiveSequence = 0;\n          }\n        }\n        tree.push(vm);\n        vm = vm.$parent;\n      }\n      return '\\n\\nfound in\\n\\n' + tree\n        .map(function (vm, i) { return (\"\" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm)\n            ? ((formatComponentName(vm[0])) + \"... (\" + (vm[1]) + \" recursive calls)\")\n            : formatComponentName(vm))); })\n        .join('\\n')\n    } else {\n      return (\"\\n\\n(found in \" + (formatComponentName(vm)) + \")\")\n    }\n  };\n}\n\n/*  */\n\n\nvar uid = 0;\n\n/**\n * A dep is an observable that can have multiple\n * directives subscribing to it.\n */\nvar Dep = function Dep () {\n  this.id = uid++;\n  this.subs = [];\n};\n\nDep.prototype.addSub = function addSub (sub) {\n  this.subs.push(sub);\n};\n\nDep.prototype.removeSub = function removeSub (sub) {\n  remove(this.subs, sub);\n};\n\nDep.prototype.depend = function depend () {\n  if (Dep.target) {\n    Dep.target.addDep(this);\n  }\n};\n\nDep.prototype.notify = function notify () {\n  // stabilize the subscriber list first\n  var subs = this.subs.slice();\n  for (var i = 0, l = subs.length; i < l; i++) {\n    subs[i].update();\n  }\n};\n\n// the current target watcher being evaluated.\n// this is globally unique because there could be only one\n// watcher being evaluated at any time.\nDep.target = null;\nvar targetStack = [];\n\nfunction pushTarget (_target) {\n  if (Dep.target) { targetStack.push(Dep.target); }\n  Dep.target = _target;\n}\n\nfunction popTarget () {\n  Dep.target = targetStack.pop();\n}\n\n/*  */\n\nvar VNode = function VNode (\n  tag,\n  data,\n  children,\n  text,\n  elm,\n  context,\n  componentOptions,\n  asyncFactory\n) {\n  this.tag = tag;\n  this.data = data;\n  this.children = children;\n  this.text = text;\n  this.elm = elm;\n  this.ns = undefined;\n  this.context = context;\n  this.fnContext = undefined;\n  this.fnOptions = undefined;\n  this.fnScopeId = undefined;\n  this.key = data && data.key;\n  this.componentOptions = componentOptions;\n  this.componentInstance = undefined;\n  this.parent = undefined;\n  this.raw = false;\n  this.isStatic = false;\n  this.isRootInsert = true;\n  this.isComment = false;\n  this.isCloned = false;\n  this.isOnce = false;\n  this.asyncFactory = asyncFactory;\n  this.asyncMeta = undefined;\n  this.isAsyncPlaceholder = false;\n};\n\nvar prototypeAccessors = { child: { configurable: true } };\n\n// DEPRECATED: alias for componentInstance for backwards compat.\n/* istanbul ignore next */\nprototypeAccessors.child.get = function () {\n  return this.componentInstance\n};\n\nObject.defineProperties( VNode.prototype, prototypeAccessors );\n\nvar createEmptyVNode = function (text) {\n  if ( text === void 0 ) text = '';\n\n  var node = new VNode();\n  node.text = text;\n  node.isComment = true;\n  return node\n};\n\nfunction createTextVNode (val) {\n  return new VNode(undefined, undefined, undefined, String(val))\n}\n\n// optimized shallow clone\n// used for static nodes and slot nodes because they may be reused across\n// multiple renders, cloning them avoids errors when DOM manipulations rely\n// on their elm reference.\nfunction cloneVNode (vnode, deep) {\n  var componentOptions = vnode.componentOptions;\n  var cloned = new VNode(\n    vnode.tag,\n    vnode.data,\n    vnode.children,\n    vnode.text,\n    vnode.elm,\n    vnode.context,\n    componentOptions,\n    vnode.asyncFactory\n  );\n  cloned.ns = vnode.ns;\n  cloned.isStatic = vnode.isStatic;\n  cloned.key = vnode.key;\n  cloned.isComment = vnode.isComment;\n  cloned.fnContext = vnode.fnContext;\n  cloned.fnOptions = vnode.fnOptions;\n  cloned.fnScopeId = vnode.fnScopeId;\n  cloned.isCloned = true;\n  if (deep) {\n    if (vnode.children) {\n      cloned.children = cloneVNodes(vnode.children, true);\n    }\n    if (componentOptions && componentOptions.children) {\n      componentOptions.children = cloneVNodes(componentOptions.children, true);\n    }\n  }\n  return cloned\n}\n\nfunction cloneVNodes (vnodes, deep) {\n  var len = vnodes.length;\n  var res = new Array(len);\n  for (var i = 0; i < len; i++) {\n    res[i] = cloneVNode(vnodes[i], deep);\n  }\n  return res\n}\n\n/*\n * not type checking this file because flow doesn't play well with\n * dynamically accessing methods on Array prototype\n */\n\nvar arrayProto = Array.prototype;\nvar arrayMethods = Object.create(arrayProto);[\n  'push',\n  'pop',\n  'shift',\n  'unshift',\n  'splice',\n  'sort',\n  'reverse'\n]\n.forEach(function (method) {\n  // cache original method\n  var original = arrayProto[method];\n  def(arrayMethods, method, function mutator () {\n    var args = [], len = arguments.length;\n    while ( len-- ) args[ len ] = arguments[ len ];\n\n    var result = original.apply(this, args);\n    var ob = this.__ob__;\n    var inserted;\n    switch (method) {\n      case 'push':\n      case 'unshift':\n        inserted = args;\n        break\n      case 'splice':\n        inserted = args.slice(2);\n        break\n    }\n    if (inserted) { ob.observeArray(inserted); }\n    // notify change\n    ob.dep.notify();\n    return result\n  });\n});\n\n/*  */\n\nvar arrayKeys = Object.getOwnPropertyNames(arrayMethods);\n\n/**\n * By default, when a reactive property is set, the new value is\n * also converted to become reactive. However when passing down props,\n * we don't want to force conversion because the value may be a nested value\n * under a frozen data structure. Converting it would defeat the optimization.\n */\nvar observerState = {\n  shouldConvert: true\n};\n\n/**\n * Observer class that are attached to each observed\n * object. Once attached, the observer converts target\n * object's property keys into getter/setters that\n * collect dependencies and dispatches updates.\n */\nvar Observer = function Observer (value) {\n  this.value = value;\n  this.dep = new Dep();\n  this.vmCount = 0;\n  def(value, '__ob__', this);\n  if (Array.isArray(value)) {\n    var augment = hasProto\n      ? protoAugment\n      : copyAugment;\n    augment(value, arrayMethods, arrayKeys);\n    this.observeArray(value);\n  } else {\n    this.walk(value);\n  }\n};\n\n/**\n * Walk through each property and convert them into\n * getter/setters. This method should only be called when\n * value type is Object.\n */\nObserver.prototype.walk = function walk (obj) {\n  var keys = Object.keys(obj);\n  for (var i = 0; i < keys.length; i++) {\n    defineReactive(obj, keys[i], obj[keys[i]]);\n  }\n};\n\n/**\n * Observe a list of Array items.\n */\nObserver.prototype.observeArray = function observeArray (items) {\n  for (var i = 0, l = items.length; i < l; i++) {\n    observe(items[i]);\n  }\n};\n\n// helpers\n\n/**\n * Augment an target Object or Array by intercepting\n * the prototype chain using __proto__\n */\nfunction protoAugment (target, src, keys) {\n  /* eslint-disable no-proto */\n  target.__proto__ = src;\n  /* eslint-enable no-proto */\n}\n\n/**\n * Augment an target Object or Array by defining\n * hidden properties.\n */\n/* istanbul ignore next */\nfunction copyAugment (target, src, keys) {\n  for (var i = 0, l = keys.length; i < l; i++) {\n    var key = keys[i];\n    def(target, key, src[key]);\n  }\n}\n\n/**\n * Attempt to create an observer instance for a value,\n * returns the new observer if successfully observed,\n * or the existing observer if the value already has one.\n */\nfunction observe (value, asRootData) {\n  if (!isObject(value) || value instanceof VNode) {\n    return\n  }\n  var ob;\n  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {\n    ob = value.__ob__;\n  } else if (\n    observerState.shouldConvert &&\n    !isServerRendering() &&\n    (Array.isArray(value) || isPlainObject(value)) &&\n    Object.isExtensible(value) &&\n    !value._isVue\n  ) {\n    ob = new Observer(value);\n  }\n  if (asRootData && ob) {\n    ob.vmCount++;\n  }\n  return ob\n}\n\n/**\n * Define a reactive property on an Object.\n */\nfunction defineReactive (\n  obj,\n  key,\n  val,\n  customSetter,\n  shallow\n) {\n  var dep = new Dep();\n\n  var property = Object.getOwnPropertyDescriptor(obj, key);\n  if (property && property.configurable === false) {\n    return\n  }\n\n  // cater for pre-defined getter/setters\n  var getter = property && property.get;\n  var setter = property && property.set;\n\n  var childOb = !shallow && observe(val);\n  Object.defineProperty(obj, key, {\n    enumerable: true,\n    configurable: true,\n    get: function reactiveGetter () {\n      var value = getter ? getter.call(obj) : val;\n      if (Dep.target) {\n        dep.depend();\n        if (childOb) {\n          childOb.dep.depend();\n          if (Array.isArray(value)) {\n            dependArray(value);\n          }\n        }\n      }\n      return value\n    },\n    set: function reactiveSetter (newVal) {\n      var value = getter ? getter.call(obj) : val;\n      /* eslint-disable no-self-compare */\n      if (newVal === value || (newVal !== newVal && value !== value)) {\n        return\n      }\n      /* eslint-enable no-self-compare */\n      if (\"development\" !== 'production' && customSetter) {\n        customSetter();\n      }\n      if (setter) {\n        setter.call(obj, newVal);\n      } else {\n        val = newVal;\n      }\n      childOb = !shallow && observe(newVal);\n      dep.notify();\n    }\n  });\n}\n\n/**\n * Set a property on an object. Adds the new property and\n * triggers change notification if the property doesn't\n * already exist.\n */\nfunction set (target, key, val) {\n  if (Array.isArray(target) && isValidArrayIndex(key)) {\n    target.length = Math.max(target.length, key);\n    target.splice(key, 1, val);\n    return val\n  }\n  if (key in target && !(key in Object.prototype)) {\n    target[key] = val;\n    return val\n  }\n  var ob = (target).__ob__;\n  if (target._isVue || (ob && ob.vmCount)) {\n    \"development\" !== 'production' && warn(\n      'Avoid adding reactive properties to a Vue instance or its root $data ' +\n      'at runtime - declare it upfront in the data option.'\n    );\n    return val\n  }\n  if (!ob) {\n    target[key] = val;\n    return val\n  }\n  defineReactive(ob.value, key, val);\n  ob.dep.notify();\n  return val\n}\n\n/**\n * Delete a property and trigger change if necessary.\n */\nfunction del (target, key) {\n  if (Array.isArray(target) && isValidArrayIndex(key)) {\n    target.splice(key, 1);\n    return\n  }\n  var ob = (target).__ob__;\n  if (target._isVue || (ob && ob.vmCount)) {\n    \"development\" !== 'production' && warn(\n      'Avoid deleting properties on a Vue instance or its root $data ' +\n      '- just set it to null.'\n    );\n    return\n  }\n  if (!hasOwn(target, key)) {\n    return\n  }\n  delete target[key];\n  if (!ob) {\n    return\n  }\n  ob.dep.notify();\n}\n\n/**\n * Collect dependencies on array elements when the array is touched, since\n * we cannot intercept array element access like property getters.\n */\nfunction dependArray (value) {\n  for (var e = (void 0), i = 0, l = value.length; i < l; i++) {\n    e = value[i];\n    e && e.__ob__ && e.__ob__.dep.depend();\n    if (Array.isArray(e)) {\n      dependArray(e);\n    }\n  }\n}\n\n/*  */\n\n/**\n * Option overwriting strategies are functions that handle\n * how to merge a parent option value and a child option\n * value into the final value.\n */\nvar strats = config.optionMergeStrategies;\n\n/**\n * Options with restrictions\n */\n{\n  strats.el = strats.propsData = function (parent, child, vm, key) {\n    if (!vm) {\n      warn(\n        \"option \\\"\" + key + \"\\\" can only be used during instance \" +\n        'creation with the `new` keyword.'\n      );\n    }\n    return defaultStrat(parent, child)\n  };\n}\n\n/**\n * Helper that recursively merges two data objects together.\n */\nfunction mergeData (to, from) {\n  if (!from) { return to }\n  var key, toVal, fromVal;\n  var keys = Object.keys(from);\n  for (var i = 0; i < keys.length; i++) {\n    key = keys[i];\n    toVal = to[key];\n    fromVal = from[key];\n    if (!hasOwn(to, key)) {\n      set(to, key, fromVal);\n    } else if (isPlainObject(toVal) && isPlainObject(fromVal)) {\n      mergeData(toVal, fromVal);\n    }\n  }\n  return to\n}\n\n/**\n * Data\n */\nfunction mergeDataOrFn (\n  parentVal,\n  childVal,\n  vm\n) {\n  if (!vm) {\n    // in a Vue.extend merge, both should be functions\n    if (!childVal) {\n      return parentVal\n    }\n    if (!parentVal) {\n      return childVal\n    }\n    // when parentVal & childVal are both present,\n    // we need to return a function that returns the\n    // merged result of both functions... no need to\n    // check if parentVal is a function here because\n    // it has to be a function to pass previous merges.\n    return function mergedDataFn () {\n      return mergeData(\n        typeof childVal === 'function' ? childVal.call(this, this) : childVal,\n        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal\n      )\n    }\n  } else {\n    return function mergedInstanceDataFn () {\n      // instance merge\n      var instanceData = typeof childVal === 'function'\n        ? childVal.call(vm, vm)\n        : childVal;\n      var defaultData = typeof parentVal === 'function'\n        ? parentVal.call(vm, vm)\n        : parentVal;\n      if (instanceData) {\n        return mergeData(instanceData, defaultData)\n      } else {\n        return defaultData\n      }\n    }\n  }\n}\n\nstrats.data = function (\n  parentVal,\n  childVal,\n  vm\n) {\n  if (!vm) {\n    if (childVal && typeof childVal !== 'function') {\n      \"development\" !== 'production' && warn(\n        'The \"data\" option should be a function ' +\n        'that returns a per-instance value in component ' +\n        'definitions.',\n        vm\n      );\n\n      return parentVal\n    }\n    return mergeDataOrFn(parentVal, childVal)\n  }\n\n  return mergeDataOrFn(parentVal, childVal, vm)\n};\n\n/**\n * Hooks and props are merged as arrays.\n */\nfunction mergeHook (\n  parentVal,\n  childVal\n) {\n  return childVal\n    ? parentVal\n      ? parentVal.concat(childVal)\n      : Array.isArray(childVal)\n        ? childVal\n        : [childVal]\n    : parentVal\n}\n\nLIFECYCLE_HOOKS.forEach(function (hook) {\n  strats[hook] = mergeHook;\n});\n\n/**\n * Assets\n *\n * When a vm is present (instance creation), we need to do\n * a three-way merge between constructor options, instance\n * options and parent options.\n */\nfunction mergeAssets (\n  parentVal,\n  childVal,\n  vm,\n  key\n) {\n  var res = Object.create(parentVal || null);\n  if (childVal) {\n    \"development\" !== 'production' && assertObjectType(key, childVal, vm);\n    return extend(res, childVal)\n  } else {\n    return res\n  }\n}\n\nASSET_TYPES.forEach(function (type) {\n  strats[type + 's'] = mergeAssets;\n});\n\n/**\n * Watchers.\n *\n * Watchers hashes should not overwrite one\n * another, so we merge them as arrays.\n */\nstrats.watch = function (\n  parentVal,\n  childVal,\n  vm,\n  key\n) {\n  // work around Firefox's Object.prototype.watch...\n  if (parentVal === nativeWatch) { parentVal = undefined; }\n  if (childVal === nativeWatch) { childVal = undefined; }\n  /* istanbul ignore if */\n  if (!childVal) { return Object.create(parentVal || null) }\n  {\n    assertObjectType(key, childVal, vm);\n  }\n  if (!parentVal) { return childVal }\n  var ret = {};\n  extend(ret, parentVal);\n  for (var key$1 in childVal) {\n    var parent = ret[key$1];\n    var child = childVal[key$1];\n    if (parent && !Array.isArray(parent)) {\n      parent = [parent];\n    }\n    ret[key$1] = parent\n      ? parent.concat(child)\n      : Array.isArray(child) ? child : [child];\n  }\n  return ret\n};\n\n/**\n * Other object hashes.\n */\nstrats.props =\nstrats.methods =\nstrats.inject =\nstrats.computed = function (\n  parentVal,\n  childVal,\n  vm,\n  key\n) {\n  if (childVal && \"development\" !== 'production') {\n    assertObjectType(key, childVal, vm);\n  }\n  if (!parentVal) { return childVal }\n  var ret = Object.create(null);\n  extend(ret, parentVal);\n  if (childVal) { extend(ret, childVal); }\n  return ret\n};\nstrats.provide = mergeDataOrFn;\n\n/**\n * Default strategy.\n */\nvar defaultStrat = function (parentVal, childVal) {\n  return childVal === undefined\n    ? parentVal\n    : childVal\n};\n\n/**\n * Validate component names\n */\nfunction checkComponents (options) {\n  for (var key in options.components) {\n    validateComponentName(key);\n  }\n}\n\nfunction validateComponentName (name) {\n  if (!/^[a-zA-Z][\\w-]*$/.test(name)) {\n    warn(\n      'Invalid component name: \"' + name + '\". Component names ' +\n      'can only contain alphanumeric characters and the hyphen, ' +\n      'and must start with a letter.'\n    );\n  }\n  var lower = name.toLowerCase();\n  if (isBuiltInTag(lower) || config.isReservedTag(lower)) {\n    warn(\n      'Do not use built-in or reserved HTML elements as component ' +\n      'id: ' + name\n    );\n  }\n}\n\n/**\n * Ensure all props option syntax are normalized into the\n * Object-based format.\n */\nfunction normalizeProps (options, vm) {\n  var props = options.props;\n  if (!props) { return }\n  var res = {};\n  var i, val, name;\n  if (Array.isArray(props)) {\n    i = props.length;\n    while (i--) {\n      val = props[i];\n      if (typeof val === 'string') {\n        name = camelize(val);\n        res[name] = { type: null };\n      } else {\n        warn('props must be strings when using array syntax.');\n      }\n    }\n  } else if (isPlainObject(props)) {\n    for (var key in props) {\n      val = props[key];\n      name = camelize(key);\n      res[name] = isPlainObject(val)\n        ? val\n        : { type: val };\n    }\n  } else {\n    warn(\n      \"Invalid value for option \\\"props\\\": expected an Array or an Object, \" +\n      \"but got \" + (toRawType(props)) + \".\",\n      vm\n    );\n  }\n  options.props = res;\n}\n\n/**\n * Normalize all injections into Object-based format\n */\nfunction normalizeInject (options, vm) {\n  var inject = options.inject;\n  var normalized = options.inject = {};\n  if (Array.isArray(inject)) {\n    for (var i = 0; i < inject.length; i++) {\n      normalized[inject[i]] = { from: inject[i] };\n    }\n  } else if (isPlainObject(inject)) {\n    for (var key in inject) {\n      var val = inject[key];\n      normalized[key] = isPlainObject(val)\n        ? extend({ from: key }, val)\n        : { from: val };\n    }\n  } else if (\"development\" !== 'production' && inject) {\n    warn(\n      \"Invalid value for option \\\"inject\\\": expected an Array or an Object, \" +\n      \"but got \" + (toRawType(inject)) + \".\",\n      vm\n    );\n  }\n}\n\n/**\n * Normalize raw function directives into object format.\n */\nfunction normalizeDirectives (options) {\n  var dirs = options.directives;\n  if (dirs) {\n    for (var key in dirs) {\n      var def = dirs[key];\n      if (typeof def === 'function') {\n        dirs[key] = { bind: def, update: def };\n      }\n    }\n  }\n}\n\nfunction assertObjectType (name, value, vm) {\n  if (!isPlainObject(value)) {\n    warn(\n      \"Invalid value for option \\\"\" + name + \"\\\": expected an Object, \" +\n      \"but got \" + (toRawType(value)) + \".\",\n      vm\n    );\n  }\n}\n\n/**\n * Merge two option objects into a new one.\n * Core utility used in both instantiation and inheritance.\n */\nfunction mergeOptions (\n  parent,\n  child,\n  vm\n) {\n  {\n    checkComponents(child);\n  }\n\n  if (typeof child === 'function') {\n    child = child.options;\n  }\n\n  normalizeProps(child, vm);\n  normalizeInject(child, vm);\n  normalizeDirectives(child);\n  var extendsFrom = child.extends;\n  if (extendsFrom) {\n    parent = mergeOptions(parent, extendsFrom, vm);\n  }\n  if (child.mixins) {\n    for (var i = 0, l = child.mixins.length; i < l; i++) {\n      parent = mergeOptions(parent, child.mixins[i], vm);\n    }\n  }\n  var options = {};\n  var key;\n  for (key in parent) {\n    mergeField(key);\n  }\n  for (key in child) {\n    if (!hasOwn(parent, key)) {\n      mergeField(key);\n    }\n  }\n  function mergeField (key) {\n    var strat = strats[key] || defaultStrat;\n    options[key] = strat(parent[key], child[key], vm, key);\n  }\n  return options\n}\n\n/**\n * Resolve an asset.\n * This function is used because child instances need access\n * to assets defined in its ancestor chain.\n */\nfunction resolveAsset (\n  options,\n  type,\n  id,\n  warnMissing\n) {\n  /* istanbul ignore if */\n  if (typeof id !== 'string') {\n    return\n  }\n  var assets = options[type];\n  // check local registration variations first\n  if (hasOwn(assets, id)) { return assets[id] }\n  var camelizedId = camelize(id);\n  if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }\n  var PascalCaseId = capitalize(camelizedId);\n  if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }\n  // fallback to prototype chain\n  var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];\n  if (\"development\" !== 'production' && warnMissing && !res) {\n    warn(\n      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,\n      options\n    );\n  }\n  return res\n}\n\n/*  */\n\nfunction validateProp (\n  key,\n  propOptions,\n  propsData,\n  vm\n) {\n  var prop = propOptions[key];\n  var absent = !hasOwn(propsData, key);\n  var value = propsData[key];\n  // handle boolean props\n  if (isType(Boolean, prop.type)) {\n    if (absent && !hasOwn(prop, 'default')) {\n      value = false;\n    } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {\n      value = true;\n    }\n  }\n  // check default value\n  if (value === undefined) {\n    value = getPropDefaultValue(vm, prop, key);\n    // since the default value is a fresh copy,\n    // make sure to observe it.\n    var prevShouldConvert = observerState.shouldConvert;\n    observerState.shouldConvert = true;\n    observe(value);\n    observerState.shouldConvert = prevShouldConvert;\n  }\n  {\n    assertProp(prop, key, value, vm, absent);\n  }\n  return value\n}\n\n/**\n * Get the default value of a prop.\n */\nfunction getPropDefaultValue (vm, prop, key) {\n  // no default, return undefined\n  if (!hasOwn(prop, 'default')) {\n    return undefined\n  }\n  var def = prop.default;\n  // warn against non-factory defaults for Object & Array\n  if (\"development\" !== 'production' && isObject(def)) {\n    warn(\n      'Invalid default value for prop \"' + key + '\": ' +\n      'Props with type Object/Array must use a factory function ' +\n      'to return the default value.',\n      vm\n    );\n  }\n  // the raw prop value was also undefined from previous render,\n  // return previous default value to avoid unnecessary watcher trigger\n  if (vm && vm.$options.propsData &&\n    vm.$options.propsData[key] === undefined &&\n    vm._props[key] !== undefined\n  ) {\n    return vm._props[key]\n  }\n  // call factory function for non-Function types\n  // a value is Function if its prototype is function even across different execution context\n  return typeof def === 'function' && getType(prop.type) !== 'Function'\n    ? def.call(vm)\n    : def\n}\n\n/**\n * Assert whether a prop is valid.\n */\nfunction assertProp (\n  prop,\n  name,\n  value,\n  vm,\n  absent\n) {\n  if (prop.required && absent) {\n    warn(\n      'Missing required prop: \"' + name + '\"',\n      vm\n    );\n    return\n  }\n  if (value == null && !prop.required) {\n    return\n  }\n  var type = prop.type;\n  var valid = !type || type === true;\n  var expectedTypes = [];\n  if (type) {\n    if (!Array.isArray(type)) {\n      type = [type];\n    }\n    for (var i = 0; i < type.length && !valid; i++) {\n      var assertedType = assertType(value, type[i]);\n      expectedTypes.push(assertedType.expectedType || '');\n      valid = assertedType.valid;\n    }\n  }\n  if (!valid) {\n    warn(\n      \"Invalid prop: type check failed for prop \\\"\" + name + \"\\\".\" +\n      \" Expected \" + (expectedTypes.map(capitalize).join(', ')) +\n      \", got \" + (toRawType(value)) + \".\",\n      vm\n    );\n    return\n  }\n  var validator = prop.validator;\n  if (validator) {\n    if (!validator(value)) {\n      warn(\n        'Invalid prop: custom validator check failed for prop \"' + name + '\".',\n        vm\n      );\n    }\n  }\n}\n\nvar simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/;\n\nfunction assertType (value, type) {\n  var valid;\n  var expectedType = getType(type);\n  if (simpleCheckRE.test(expectedType)) {\n    var t = typeof value;\n    valid = t === expectedType.toLowerCase();\n    // for primitive wrapper objects\n    if (!valid && t === 'object') {\n      valid = value instanceof type;\n    }\n  } else if (expectedType === 'Object') {\n    valid = isPlainObject(value);\n  } else if (expectedType === 'Array') {\n    valid = Array.isArray(value);\n  } else {\n    valid = value instanceof type;\n  }\n  return {\n    valid: valid,\n    expectedType: expectedType\n  }\n}\n\n/**\n * Use function string name to check built-in types,\n * because a simple equality check will fail when running\n * across different vms / iframes.\n */\nfunction getType (fn) {\n  var match = fn && fn.toString().match(/^\\s*function (\\w+)/);\n  return match ? match[1] : ''\n}\n\nfunction isType (type, fn) {\n  if (!Array.isArray(fn)) {\n    return getType(fn) === getType(type)\n  }\n  for (var i = 0, len = fn.length; i < len; i++) {\n    if (getType(fn[i]) === getType(type)) {\n      return true\n    }\n  }\n  /* istanbul ignore next */\n  return false\n}\n\n/*  */\n\nfunction handleError (err, vm, info) {\n  if (vm) {\n    var cur = vm;\n    while ((cur = cur.$parent)) {\n      var hooks = cur.$options.errorCaptured;\n      if (hooks) {\n        for (var i = 0; i < hooks.length; i++) {\n          try {\n            var capture = hooks[i].call(cur, err, vm, info) === false;\n            if (capture) { return }\n          } catch (e) {\n            globalHandleError(e, cur, 'errorCaptured hook');\n          }\n        }\n      }\n    }\n  }\n  globalHandleError(err, vm, info);\n}\n\nfunction globalHandleError (err, vm, info) {\n  if (config.errorHandler) {\n    try {\n      return config.errorHandler.call(null, err, vm, info)\n    } catch (e) {\n      logError(e, null, 'config.errorHandler');\n    }\n  }\n  logError(err, vm, info);\n}\n\nfunction logError (err, vm, info) {\n  {\n    warn((\"Error in \" + info + \": \\\"\" + (err.toString()) + \"\\\"\"), vm);\n  }\n  /* istanbul ignore else */\n  if ((inBrowser || inWeex) && typeof console !== 'undefined') {\n    console.error(err);\n  } else {\n    throw err\n  }\n}\n\n/*  */\n/* globals MessageChannel */\n\nvar callbacks = [];\nvar pending = false;\n\nfunction flushCallbacks () {\n  pending = false;\n  var copies = callbacks.slice(0);\n  callbacks.length = 0;\n  for (var i = 0; i < copies.length; i++) {\n    copies[i]();\n  }\n}\n\n// Here we have async deferring wrappers using both micro and macro tasks.\n// In < 2.4 we used micro tasks everywhere, but there are some scenarios where\n// micro tasks have too high a priority and fires in between supposedly\n// sequential events (e.g. #4521, #6690) or even between bubbling of the same\n// event (#6566). However, using macro tasks everywhere also has subtle problems\n// when state is changed right before repaint (e.g. #6813, out-in transitions).\n// Here we use micro task by default, but expose a way to force macro task when\n// needed (e.g. in event handlers attached by v-on).\nvar microTimerFunc;\nvar macroTimerFunc;\nvar useMacroTask = false;\n\n// Determine (macro) Task defer implementation.\n// Technically setImmediate should be the ideal choice, but it's only available\n// in IE. The only polyfill that consistently queues the callback after all DOM\n// events triggered in the same loop is by using MessageChannel.\n/* istanbul ignore if */\nif (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {\n  macroTimerFunc = function () {\n    setImmediate(flushCallbacks);\n  };\n} else if (typeof MessageChannel !== 'undefined' && (\n  isNative(MessageChannel) ||\n  // PhantomJS\n  MessageChannel.toString() === '[object MessageChannelConstructor]'\n)) {\n  var channel = new MessageChannel();\n  var port = channel.port2;\n  channel.port1.onmessage = flushCallbacks;\n  macroTimerFunc = function () {\n    port.postMessage(1);\n  };\n} else {\n  /* istanbul ignore next */\n  macroTimerFunc = function () {\n    setTimeout(flushCallbacks, 0);\n  };\n}\n\n// Determine MicroTask defer implementation.\n/* istanbul ignore next, $flow-disable-line */\nif (typeof Promise !== 'undefined' && isNative(Promise)) {\n  var p = Promise.resolve();\n  microTimerFunc = function () {\n    p.then(flushCallbacks);\n    // in problematic UIWebViews, Promise.then doesn't completely break, but\n    // it can get stuck in a weird state where callbacks are pushed into the\n    // microtask queue but the queue isn't being flushed, until the browser\n    // needs to do some other work, e.g. handle a timer. Therefore we can\n    // \"force\" the microtask queue to be flushed by adding an empty timer.\n    if (isIOS) { setTimeout(noop); }\n  };\n} else {\n  // fallback to macro\n  microTimerFunc = macroTimerFunc;\n}\n\n/**\n * Wrap a function so that if any code inside triggers state change,\n * the changes are queued using a Task instead of a MicroTask.\n */\nfunction withMacroTask (fn) {\n  return fn._withTask || (fn._withTask = function () {\n    useMacroTask = true;\n    var res = fn.apply(null, arguments);\n    useMacroTask = false;\n    return res\n  })\n}\n\nfunction nextTick (cb, ctx) {\n  var _resolve;\n  callbacks.push(function () {\n    if (cb) {\n      try {\n        cb.call(ctx);\n      } catch (e) {\n        handleError(e, ctx, 'nextTick');\n      }\n    } else if (_resolve) {\n      _resolve(ctx);\n    }\n  });\n  if (!pending) {\n    pending = true;\n    if (useMacroTask) {\n      macroTimerFunc();\n    } else {\n      microTimerFunc();\n    }\n  }\n  // $flow-disable-line\n  if (!cb && typeof Promise !== 'undefined') {\n    return new Promise(function (resolve) {\n      _resolve = resolve;\n    })\n  }\n}\n\n/*  */\n\nvar mark;\nvar measure;\n\n{\n  var perf = inBrowser && window.performance;\n  /* istanbul ignore if */\n  if (\n    perf &&\n    perf.mark &&\n    perf.measure &&\n    perf.clearMarks &&\n    perf.clearMeasures\n  ) {\n    mark = function (tag) { return perf.mark(tag); };\n    measure = function (name, startTag, endTag) {\n      perf.measure(name, startTag, endTag);\n      perf.clearMarks(startTag);\n      perf.clearMarks(endTag);\n      perf.clearMeasures(name);\n    };\n  }\n}\n\n/* not type checking this file because flow doesn't play well with Proxy */\n\nvar initProxy;\n\n{\n  var allowedGlobals = makeMap(\n    'Infinity,undefined,NaN,isFinite,isNaN,' +\n    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +\n    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +\n    'require' // for Webpack/Browserify\n  );\n\n  var warnNonPresent = function (target, key) {\n    warn(\n      \"Property or method \\\"\" + key + \"\\\" is not defined on the instance but \" +\n      'referenced during render. Make sure that this property is reactive, ' +\n      'either in the data option, or for class-based components, by ' +\n      'initializing the property. ' +\n      'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',\n      target\n    );\n  };\n\n  var hasProxy =\n    typeof Proxy !== 'undefined' &&\n    Proxy.toString().match(/native code/);\n\n  if (hasProxy) {\n    var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact');\n    config.keyCodes = new Proxy(config.keyCodes, {\n      set: function set (target, key, value) {\n        if (isBuiltInModifier(key)) {\n          warn((\"Avoid overwriting built-in modifier in config.keyCodes: .\" + key));\n          return false\n        } else {\n          target[key] = value;\n          return true\n        }\n      }\n    });\n  }\n\n  var hasHandler = {\n    has: function has (target, key) {\n      var has = key in target;\n      var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';\n      if (!has && !isAllowed) {\n        warnNonPresent(target, key);\n      }\n      return has || !isAllowed\n    }\n  };\n\n  var getHandler = {\n    get: function get (target, key) {\n      if (typeof key === 'string' && !(key in target)) {\n        warnNonPresent(target, key);\n      }\n      return target[key]\n    }\n  };\n\n  initProxy = function initProxy (vm) {\n    if (hasProxy) {\n      // determine which proxy handler to use\n      var options = vm.$options;\n      var handlers = options.render && options.render._withStripped\n        ? getHandler\n        : hasHandler;\n      vm._renderProxy = new Proxy(vm, handlers);\n    } else {\n      vm._renderProxy = vm;\n    }\n  };\n}\n\n/*  */\n\nvar seenObjects = new _Set();\n\n/**\n * Recursively traverse an object to evoke all converted\n * getters, so that every nested property inside the object\n * is collected as a \"deep\" dependency.\n */\nfunction traverse (val) {\n  _traverse(val, seenObjects);\n  seenObjects.clear();\n}\n\nfunction _traverse (val, seen) {\n  var i, keys;\n  var isA = Array.isArray(val);\n  if ((!isA && !isObject(val)) || Object.isFrozen(val)) {\n    return\n  }\n  if (val.__ob__) {\n    var depId = val.__ob__.dep.id;\n    if (seen.has(depId)) {\n      return\n    }\n    seen.add(depId);\n  }\n  if (isA) {\n    i = val.length;\n    while (i--) { _traverse(val[i], seen); }\n  } else {\n    keys = Object.keys(val);\n    i = keys.length;\n    while (i--) { _traverse(val[keys[i]], seen); }\n  }\n}\n\n/*  */\n\nvar normalizeEvent = cached(function (name) {\n  var passive = name.charAt(0) === '&';\n  name = passive ? name.slice(1) : name;\n  var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first\n  name = once$$1 ? name.slice(1) : name;\n  var capture = name.charAt(0) === '!';\n  name = capture ? name.slice(1) : name;\n  return {\n    name: name,\n    once: once$$1,\n    capture: capture,\n    passive: passive\n  }\n});\n\nfunction createFnInvoker (fns) {\n  function invoker () {\n    var arguments$1 = arguments;\n\n    var fns = invoker.fns;\n    if (Array.isArray(fns)) {\n      var cloned = fns.slice();\n      for (var i = 0; i < cloned.length; i++) {\n        cloned[i].apply(null, arguments$1);\n      }\n    } else {\n      // return handler return value for single handlers\n      return fns.apply(null, arguments)\n    }\n  }\n  invoker.fns = fns;\n  return invoker\n}\n\nfunction updateListeners (\n  on,\n  oldOn,\n  add,\n  remove$$1,\n  vm\n) {\n  var name, cur, old, event;\n  for (name in on) {\n    cur = on[name];\n    old = oldOn[name];\n    event = normalizeEvent(name);\n    if (isUndef(cur)) {\n      \"development\" !== 'production' && warn(\n        \"Invalid handler for event \\\"\" + (event.name) + \"\\\": got \" + String(cur),\n        vm\n      );\n    } else if (isUndef(old)) {\n      if (isUndef(cur.fns)) {\n        cur = on[name] = createFnInvoker(cur);\n      }\n      add(event.name, cur, event.once, event.capture, event.passive);\n    } else if (cur !== old) {\n      old.fns = cur;\n      on[name] = old;\n    }\n  }\n  for (name in oldOn) {\n    if (isUndef(on[name])) {\n      event = normalizeEvent(name);\n      remove$$1(event.name, oldOn[name], event.capture);\n    }\n  }\n}\n\n/*  */\n\nfunction mergeVNodeHook (def, hookKey, hook) {\n  if (def instanceof VNode) {\n    def = def.data.hook || (def.data.hook = {});\n  }\n  var invoker;\n  var oldHook = def[hookKey];\n\n  function wrappedHook () {\n    hook.apply(this, arguments);\n    // important: remove merged hook to ensure it's called only once\n    // and prevent memory leak\n    remove(invoker.fns, wrappedHook);\n  }\n\n  if (isUndef(oldHook)) {\n    // no existing hook\n    invoker = createFnInvoker([wrappedHook]);\n  } else {\n    /* istanbul ignore if */\n    if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {\n      // already a merged invoker\n      invoker = oldHook;\n      invoker.fns.push(wrappedHook);\n    } else {\n      // existing plain hook\n      invoker = createFnInvoker([oldHook, wrappedHook]);\n    }\n  }\n\n  invoker.merged = true;\n  def[hookKey] = invoker;\n}\n\n/*  */\n\nfunction extractPropsFromVNodeData (\n  data,\n  Ctor,\n  tag\n) {\n  // we are only extracting raw values here.\n  // validation and default values are handled in the child\n  // component itself.\n  var propOptions = Ctor.options.props;\n  if (isUndef(propOptions)) {\n    return\n  }\n  var res = {};\n  var attrs = data.attrs;\n  var props = data.props;\n  if (isDef(attrs) || isDef(props)) {\n    for (var key in propOptions) {\n      var altKey = hyphenate(key);\n      {\n        var keyInLowerCase = key.toLowerCase();\n        if (\n          key !== keyInLowerCase &&\n          attrs && hasOwn(attrs, keyInLowerCase)\n        ) {\n          tip(\n            \"Prop \\\"\" + keyInLowerCase + \"\\\" is passed to component \" +\n            (formatComponentName(tag || Ctor)) + \", but the declared prop name is\" +\n            \" \\\"\" + key + \"\\\". \" +\n            \"Note that HTML attributes are case-insensitive and camelCased \" +\n            \"props need to use their kebab-case equivalents when using in-DOM \" +\n            \"templates. You should probably use \\\"\" + altKey + \"\\\" instead of \\\"\" + key + \"\\\".\"\n          );\n        }\n      }\n      checkProp(res, props, key, altKey, true) ||\n      checkProp(res, attrs, key, altKey, false);\n    }\n  }\n  return res\n}\n\nfunction checkProp (\n  res,\n  hash,\n  key,\n  altKey,\n  preserve\n) {\n  if (isDef(hash)) {\n    if (hasOwn(hash, key)) {\n      res[key] = hash[key];\n      if (!preserve) {\n        delete hash[key];\n      }\n      return true\n    } else if (hasOwn(hash, altKey)) {\n      res[key] = hash[altKey];\n      if (!preserve) {\n        delete hash[altKey];\n      }\n      return true\n    }\n  }\n  return false\n}\n\n/*  */\n\n// The template compiler attempts to minimize the need for normalization by\n// statically analyzing the template at compile time.\n//\n// For plain HTML markup, normalization can be completely skipped because the\n// generated render function is guaranteed to return Array<VNode>. There are\n// two cases where extra normalization is needed:\n\n// 1. When the children contains components - because a functional component\n// may return an Array instead of a single root. In this case, just a simple\n// normalization is needed - if any child is an Array, we flatten the whole\n// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep\n// because functional components already normalize their own children.\nfunction simpleNormalizeChildren (children) {\n  for (var i = 0; i < children.length; i++) {\n    if (Array.isArray(children[i])) {\n      return Array.prototype.concat.apply([], children)\n    }\n  }\n  return children\n}\n\n// 2. When the children contains constructs that always generated nested Arrays,\n// e.g. <template>, <slot>, v-for, or when the children is provided by user\n// with hand-written render functions / JSX. In such cases a full normalization\n// is needed to cater to all possible types of children values.\nfunction normalizeChildren (children) {\n  return isPrimitive(children)\n    ? [createTextVNode(children)]\n    : Array.isArray(children)\n      ? normalizeArrayChildren(children)\n      : undefined\n}\n\nfunction isTextNode (node) {\n  return isDef(node) && isDef(node.text) && isFalse(node.isComment)\n}\n\nfunction normalizeArrayChildren (children, nestedIndex) {\n  var res = [];\n  var i, c, lastIndex, last;\n  for (i = 0; i < children.length; i++) {\n    c = children[i];\n    if (isUndef(c) || typeof c === 'boolean') { continue }\n    lastIndex = res.length - 1;\n    last = res[lastIndex];\n    //  nested\n    if (Array.isArray(c)) {\n      if (c.length > 0) {\n        c = normalizeArrayChildren(c, ((nestedIndex || '') + \"_\" + i));\n        // merge adjacent text nodes\n        if (isTextNode(c[0]) && isTextNode(last)) {\n          res[lastIndex] = createTextVNode(last.text + (c[0]).text);\n          c.shift();\n        }\n        res.push.apply(res, c);\n      }\n    } else if (isPrimitive(c)) {\n      if (isTextNode(last)) {\n        // merge adjacent text nodes\n        // this is necessary for SSR hydration because text nodes are\n        // essentially merged when rendered to HTML strings\n        res[lastIndex] = createTextVNode(last.text + c);\n      } else if (c !== '') {\n        // convert primitive to vnode\n        res.push(createTextVNode(c));\n      }\n    } else {\n      if (isTextNode(c) && isTextNode(last)) {\n        // merge adjacent text nodes\n        res[lastIndex] = createTextVNode(last.text + c.text);\n      } else {\n        // default key for nested array children (likely generated by v-for)\n        if (isTrue(children._isVList) &&\n          isDef(c.tag) &&\n          isUndef(c.key) &&\n          isDef(nestedIndex)) {\n          c.key = \"__vlist\" + nestedIndex + \"_\" + i + \"__\";\n        }\n        res.push(c);\n      }\n    }\n  }\n  return res\n}\n\n/*  */\n\nfunction ensureCtor (comp, base) {\n  if (\n    comp.__esModule ||\n    (hasSymbol && comp[Symbol.toStringTag] === 'Module')\n  ) {\n    comp = comp.default;\n  }\n  return isObject(comp)\n    ? base.extend(comp)\n    : comp\n}\n\nfunction createAsyncPlaceholder (\n  factory,\n  data,\n  context,\n  children,\n  tag\n) {\n  var node = createEmptyVNode();\n  node.asyncFactory = factory;\n  node.asyncMeta = { data: data, context: context, children: children, tag: tag };\n  return node\n}\n\nfunction resolveAsyncComponent (\n  factory,\n  baseCtor,\n  context\n) {\n  if (isTrue(factory.error) && isDef(factory.errorComp)) {\n    return factory.errorComp\n  }\n\n  if (isDef(factory.resolved)) {\n    return factory.resolved\n  }\n\n  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {\n    return factory.loadingComp\n  }\n\n  if (isDef(factory.contexts)) {\n    // already pending\n    factory.contexts.push(context);\n  } else {\n    var contexts = factory.contexts = [context];\n    var sync = true;\n\n    var forceRender = function () {\n      for (var i = 0, l = contexts.length; i < l; i++) {\n        contexts[i].$forceUpdate();\n      }\n    };\n\n    var resolve = once(function (res) {\n      // cache resolved\n      factory.resolved = ensureCtor(res, baseCtor);\n      // invoke callbacks only if this is not a synchronous resolve\n      // (async resolves are shimmed as synchronous during SSR)\n      if (!sync) {\n        forceRender();\n      }\n    });\n\n    var reject = once(function (reason) {\n      \"development\" !== 'production' && warn(\n        \"Failed to resolve async component: \" + (String(factory)) +\n        (reason ? (\"\\nReason: \" + reason) : '')\n      );\n      if (isDef(factory.errorComp)) {\n        factory.error = true;\n        forceRender();\n      }\n    });\n\n    var res = factory(resolve, reject);\n\n    if (isObject(res)) {\n      if (typeof res.then === 'function') {\n        // () => Promise\n        if (isUndef(factory.resolved)) {\n          res.then(resolve, reject);\n        }\n      } else if (isDef(res.component) && typeof res.component.then === 'function') {\n        res.component.then(resolve, reject);\n\n        if (isDef(res.error)) {\n          factory.errorComp = ensureCtor(res.error, baseCtor);\n        }\n\n        if (isDef(res.loading)) {\n          factory.loadingComp = ensureCtor(res.loading, baseCtor);\n          if (res.delay === 0) {\n            factory.loading = true;\n          } else {\n            setTimeout(function () {\n              if (isUndef(factory.resolved) && isUndef(factory.error)) {\n                factory.loading = true;\n                forceRender();\n              }\n            }, res.delay || 200);\n          }\n        }\n\n        if (isDef(res.timeout)) {\n          setTimeout(function () {\n            if (isUndef(factory.resolved)) {\n              reject(\n                \"timeout (\" + (res.timeout) + \"ms)\"\n              );\n            }\n          }, res.timeout);\n        }\n      }\n    }\n\n    sync = false;\n    // return in case resolved synchronously\n    return factory.loading\n      ? factory.loadingComp\n      : factory.resolved\n  }\n}\n\n/*  */\n\nfunction isAsyncPlaceholder (node) {\n  return node.isComment && node.asyncFactory\n}\n\n/*  */\n\nfunction getFirstComponentChild (children) {\n  if (Array.isArray(children)) {\n    for (var i = 0; i < children.length; i++) {\n      var c = children[i];\n      if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {\n        return c\n      }\n    }\n  }\n}\n\n/*  */\n\n/*  */\n\nfunction initEvents (vm) {\n  vm._events = Object.create(null);\n  vm._hasHookEvent = false;\n  // init parent attached events\n  var listeners = vm.$options._parentListeners;\n  if (listeners) {\n    updateComponentListeners(vm, listeners);\n  }\n}\n\nvar target;\n\nfunction add (event, fn, once) {\n  if (once) {\n    target.$once(event, fn);\n  } else {\n    target.$on(event, fn);\n  }\n}\n\nfunction remove$1 (event, fn) {\n  target.$off(event, fn);\n}\n\nfunction updateComponentListeners (\n  vm,\n  listeners,\n  oldListeners\n) {\n  target = vm;\n  updateListeners(listeners, oldListeners || {}, add, remove$1, vm);\n  target = undefined;\n}\n\nfunction eventsMixin (Vue) {\n  var hookRE = /^hook:/;\n  Vue.prototype.$on = function (event, fn) {\n    var this$1 = this;\n\n    var vm = this;\n    if (Array.isArray(event)) {\n      for (var i = 0, l = event.length; i < l; i++) {\n        this$1.$on(event[i], fn);\n      }\n    } else {\n      (vm._events[event] || (vm._events[event] = [])).push(fn);\n      // optimize hook:event cost by using a boolean flag marked at registration\n      // instead of a hash lookup\n      if (hookRE.test(event)) {\n        vm._hasHookEvent = true;\n      }\n    }\n    return vm\n  };\n\n  Vue.prototype.$once = function (event, fn) {\n    var vm = this;\n    function on () {\n      vm.$off(event, on);\n      fn.apply(vm, arguments);\n    }\n    on.fn = fn;\n    vm.$on(event, on);\n    return vm\n  };\n\n  Vue.prototype.$off = function (event, fn) {\n    var this$1 = this;\n\n    var vm = this;\n    // all\n    if (!arguments.length) {\n      vm._events = Object.create(null);\n      return vm\n    }\n    // array of events\n    if (Array.isArray(event)) {\n      for (var i = 0, l = event.length; i < l; i++) {\n        this$1.$off(event[i], fn);\n      }\n      return vm\n    }\n    // specific event\n    var cbs = vm._events[event];\n    if (!cbs) {\n      return vm\n    }\n    if (!fn) {\n      vm._events[event] = null;\n      return vm\n    }\n    if (fn) {\n      // specific handler\n      var cb;\n      var i$1 = cbs.length;\n      while (i$1--) {\n        cb = cbs[i$1];\n        if (cb === fn || cb.fn === fn) {\n          cbs.splice(i$1, 1);\n          break\n        }\n      }\n    }\n    return vm\n  };\n\n  Vue.prototype.$emit = function (event) {\n    var vm = this;\n    {\n      var lowerCaseEvent = event.toLowerCase();\n      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {\n        tip(\n          \"Event \\\"\" + lowerCaseEvent + \"\\\" is emitted in component \" +\n          (formatComponentName(vm)) + \" but the handler is registered for \\\"\" + event + \"\\\". \" +\n          \"Note that HTML attributes are case-insensitive and you cannot use \" +\n          \"v-on to listen to camelCase events when using in-DOM templates. \" +\n          \"You should probably use \\\"\" + (hyphenate(event)) + \"\\\" instead of \\\"\" + event + \"\\\".\"\n        );\n      }\n    }\n    var cbs = vm._events[event];\n    if (cbs) {\n      cbs = cbs.length > 1 ? toArray(cbs) : cbs;\n      var args = toArray(arguments, 1);\n      for (var i = 0, l = cbs.length; i < l; i++) {\n        try {\n          cbs[i].apply(vm, args);\n        } catch (e) {\n          handleError(e, vm, (\"event handler for \\\"\" + event + \"\\\"\"));\n        }\n      }\n    }\n    return vm\n  };\n}\n\n/*  */\n\n/**\n * Runtime helper for resolving raw children VNodes into a slot object.\n */\nfunction resolveSlots (\n  children,\n  context\n) {\n  var slots = {};\n  if (!children) {\n    return slots\n  }\n  for (var i = 0, l = children.length; i < l; i++) {\n    var child = children[i];\n    var data = child.data;\n    // remove slot attribute if the node is resolved as a Vue slot node\n    if (data && data.attrs && data.attrs.slot) {\n      delete data.attrs.slot;\n    }\n    // named slots should only be respected if the vnode was rendered in the\n    // same context.\n    if ((child.context === context || child.fnContext === context) &&\n      data && data.slot != null\n    ) {\n      var name = child.data.slot;\n      var slot = (slots[name] || (slots[name] = []));\n      if (child.tag === 'template') {\n        slot.push.apply(slot, child.children);\n      } else {\n        slot.push(child);\n      }\n    } else {\n      (slots.default || (slots.default = [])).push(child);\n    }\n  }\n  // ignore slots that contains only whitespace\n  for (var name$1 in slots) {\n    if (slots[name$1].every(isWhitespace)) {\n      delete slots[name$1];\n    }\n  }\n  return slots\n}\n\nfunction isWhitespace (node) {\n  return (node.isComment && !node.asyncFactory) || node.text === ' '\n}\n\nfunction resolveScopedSlots (\n  fns, // see flow/vnode\n  res\n) {\n  res = res || {};\n  for (var i = 0; i < fns.length; i++) {\n    if (Array.isArray(fns[i])) {\n      resolveScopedSlots(fns[i], res);\n    } else {\n      res[fns[i].key] = fns[i].fn;\n    }\n  }\n  return res\n}\n\n/*  */\n\nvar activeInstance = null;\nvar isUpdatingChildComponent = false;\n\nfunction initLifecycle (vm) {\n  var options = vm.$options;\n\n  // locate first non-abstract parent\n  var parent = options.parent;\n  if (parent && !options.abstract) {\n    while (parent.$options.abstract && parent.$parent) {\n      parent = parent.$parent;\n    }\n    parent.$children.push(vm);\n  }\n\n  vm.$parent = parent;\n  vm.$root = parent ? parent.$root : vm;\n\n  vm.$children = [];\n  vm.$refs = {};\n\n  vm._watcher = null;\n  vm._inactive = null;\n  vm._directInactive = false;\n  vm._isMounted = false;\n  vm._isDestroyed = false;\n  vm._isBeingDestroyed = false;\n}\n\nfunction lifecycleMixin (Vue) {\n  Vue.prototype._update = function (vnode, hydrating) {\n    var vm = this;\n    if (vm._isMounted) {\n      callHook(vm, 'beforeUpdate');\n    }\n    var prevEl = vm.$el;\n    var prevVnode = vm._vnode;\n    var prevActiveInstance = activeInstance;\n    activeInstance = vm;\n    vm._vnode = vnode;\n    // Vue.prototype.__patch__ is injected in entry points\n    // based on the rendering backend used.\n    if (!prevVnode) {\n      // initial render\n      vm.$el = vm.__patch__(\n        vm.$el, vnode, hydrating, false /* removeOnly */,\n        vm.$options._parentElm,\n        vm.$options._refElm\n      );\n      // no need for the ref nodes after initial patch\n      // this prevents keeping a detached DOM tree in memory (#5851)\n      vm.$options._parentElm = vm.$options._refElm = null;\n    } else {\n      // updates\n      vm.$el = vm.__patch__(prevVnode, vnode);\n    }\n    activeInstance = prevActiveInstance;\n    // update __vue__ reference\n    if (prevEl) {\n      prevEl.__vue__ = null;\n    }\n    if (vm.$el) {\n      vm.$el.__vue__ = vm;\n    }\n    // if parent is an HOC, update its $el as well\n    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {\n      vm.$parent.$el = vm.$el;\n    }\n    // updated hook is called by the scheduler to ensure that children are\n    // updated in a parent's updated hook.\n  };\n\n  Vue.prototype.$forceUpdate = function () {\n    var vm = this;\n    if (vm._watcher) {\n      vm._watcher.update();\n    }\n  };\n\n  Vue.prototype.$destroy = function () {\n    var vm = this;\n    if (vm._isBeingDestroyed) {\n      return\n    }\n    callHook(vm, 'beforeDestroy');\n    vm._isBeingDestroyed = true;\n    // remove self from parent\n    var parent = vm.$parent;\n    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {\n      remove(parent.$children, vm);\n    }\n    // teardown watchers\n    if (vm._watcher) {\n      vm._watcher.teardown();\n    }\n    var i = vm._watchers.length;\n    while (i--) {\n      vm._watchers[i].teardown();\n    }\n    // remove reference from data ob\n    // frozen object may not have observer.\n    if (vm._data.__ob__) {\n      vm._data.__ob__.vmCount--;\n    }\n    // call the last hook...\n    vm._isDestroyed = true;\n    // invoke destroy hooks on current rendered tree\n    vm.__patch__(vm._vnode, null);\n    // fire destroyed hook\n    callHook(vm, 'destroyed');\n    // turn off all instance listeners.\n    vm.$off();\n    // remove __vue__ reference\n    if (vm.$el) {\n      vm.$el.__vue__ = null;\n    }\n    // release circular reference (#6759)\n    if (vm.$vnode) {\n      vm.$vnode.parent = null;\n    }\n  };\n}\n\nfunction mountComponent (\n  vm,\n  el,\n  hydrating\n) {\n  vm.$el = el;\n  if (!vm.$options.render) {\n    vm.$options.render = createEmptyVNode;\n    {\n      /* istanbul ignore if */\n      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||\n        vm.$options.el || el) {\n        warn(\n          'You are using the runtime-only build of Vue where the template ' +\n          'compiler is not available. Either pre-compile the templates into ' +\n          'render functions, or use the compiler-included build.',\n          vm\n        );\n      } else {\n        warn(\n          'Failed to mount component: template or render function not defined.',\n          vm\n        );\n      }\n    }\n  }\n  callHook(vm, 'beforeMount');\n\n  var updateComponent;\n  /* istanbul ignore if */\n  if (\"development\" !== 'production' && config.performance && mark) {\n    updateComponent = function () {\n      var name = vm._name;\n      var id = vm._uid;\n      var startTag = \"vue-perf-start:\" + id;\n      var endTag = \"vue-perf-end:\" + id;\n\n      mark(startTag);\n      var vnode = vm._render();\n      mark(endTag);\n      measure((\"vue \" + name + \" render\"), startTag, endTag);\n\n      mark(startTag);\n      vm._update(vnode, hydrating);\n      mark(endTag);\n      measure((\"vue \" + name + \" patch\"), startTag, endTag);\n    };\n  } else {\n    updateComponent = function () {\n      vm._update(vm._render(), hydrating);\n    };\n  }\n\n  // we set this to vm._watcher inside the watcher's constructor\n  // since the watcher's initial patch may call $forceUpdate (e.g. inside child\n  // component's mounted hook), which relies on vm._watcher being already defined\n  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);\n  hydrating = false;\n\n  // manually mounted instance, call mounted on self\n  // mounted is called for render-created child components in its inserted hook\n  if (vm.$vnode == null) {\n    vm._isMounted = true;\n    callHook(vm, 'mounted');\n  }\n  return vm\n}\n\nfunction updateChildComponent (\n  vm,\n  propsData,\n  listeners,\n  parentVnode,\n  renderChildren\n) {\n  {\n    isUpdatingChildComponent = true;\n  }\n\n  // determine whether component has slot children\n  // we need to do this before overwriting $options._renderChildren\n  var hasChildren = !!(\n    renderChildren ||               // has new static slots\n    vm.$options._renderChildren ||  // has old static slots\n    parentVnode.data.scopedSlots || // has new scoped slots\n    vm.$scopedSlots !== emptyObject // has old scoped slots\n  );\n\n  vm.$options._parentVnode = parentVnode;\n  vm.$vnode = parentVnode; // update vm's placeholder node without re-render\n\n  if (vm._vnode) { // update child tree's parent\n    vm._vnode.parent = parentVnode;\n  }\n  vm.$options._renderChildren = renderChildren;\n\n  // update $attrs and $listeners hash\n  // these are also reactive so they may trigger child update if the child\n  // used them during render\n  vm.$attrs = (parentVnode.data && parentVnode.data.attrs) || emptyObject;\n  vm.$listeners = listeners || emptyObject;\n\n  // update props\n  if (propsData && vm.$options.props) {\n    observerState.shouldConvert = false;\n    var props = vm._props;\n    var propKeys = vm.$options._propKeys || [];\n    for (var i = 0; i < propKeys.length; i++) {\n      var key = propKeys[i];\n      props[key] = validateProp(key, vm.$options.props, propsData, vm);\n    }\n    observerState.shouldConvert = true;\n    // keep a copy of raw propsData\n    vm.$options.propsData = propsData;\n  }\n\n  // update listeners\n  if (listeners) {\n    var oldListeners = vm.$options._parentListeners;\n    vm.$options._parentListeners = listeners;\n    updateComponentListeners(vm, listeners, oldListeners);\n  }\n  // resolve slots + force update if has children\n  if (hasChildren) {\n    vm.$slots = resolveSlots(renderChildren, parentVnode.context);\n    vm.$forceUpdate();\n  }\n\n  {\n    isUpdatingChildComponent = false;\n  }\n}\n\nfunction isInInactiveTree (vm) {\n  while (vm && (vm = vm.$parent)) {\n    if (vm._inactive) { return true }\n  }\n  return false\n}\n\nfunction activateChildComponent (vm, direct) {\n  if (direct) {\n    vm._directInactive = false;\n    if (isInInactiveTree(vm)) {\n      return\n    }\n  } else if (vm._directInactive) {\n    return\n  }\n  if (vm._inactive || vm._inactive === null) {\n    vm._inactive = false;\n    for (var i = 0; i < vm.$children.length; i++) {\n      activateChildComponent(vm.$children[i]);\n    }\n    callHook(vm, 'activated');\n  }\n}\n\nfunction deactivateChildComponent (vm, direct) {\n  if (direct) {\n    vm._directInactive = true;\n    if (isInInactiveTree(vm)) {\n      return\n    }\n  }\n  if (!vm._inactive) {\n    vm._inactive = true;\n    for (var i = 0; i < vm.$children.length; i++) {\n      deactivateChildComponent(vm.$children[i]);\n    }\n    callHook(vm, 'deactivated');\n  }\n}\n\nfunction callHook (vm, hook) {\n  var handlers = vm.$options[hook];\n  if (handlers) {\n    for (var i = 0, j = handlers.length; i < j; i++) {\n      try {\n        handlers[i].call(vm);\n      } catch (e) {\n        handleError(e, vm, (hook + \" hook\"));\n      }\n    }\n  }\n  if (vm._hasHookEvent) {\n    vm.$emit('hook:' + hook);\n  }\n}\n\n/*  */\n\n\nvar MAX_UPDATE_COUNT = 100;\n\nvar queue = [];\nvar activatedChildren = [];\nvar has = {};\nvar circular = {};\nvar waiting = false;\nvar flushing = false;\nvar index = 0;\n\n/**\n * Reset the scheduler's state.\n */\nfunction resetSchedulerState () {\n  index = queue.length = activatedChildren.length = 0;\n  has = {};\n  {\n    circular = {};\n  }\n  waiting = flushing = false;\n}\n\n/**\n * Flush both queues and run the watchers.\n */\nfunction flushSchedulerQueue () {\n  flushing = true;\n  var watcher, id;\n\n  // Sort queue before flush.\n  // This ensures that:\n  // 1. Components are updated from parent to child. (because parent is always\n  //    created before the child)\n  // 2. A component's user watchers are run before its render watcher (because\n  //    user watchers are created before the render watcher)\n  // 3. If a component is destroyed during a parent component's watcher run,\n  //    its watchers can be skipped.\n  queue.sort(function (a, b) { return a.id - b.id; });\n\n  // do not cache length because more watchers might be pushed\n  // as we run existing watchers\n  for (index = 0; index < queue.length; index++) {\n    watcher = queue[index];\n    id = watcher.id;\n    has[id] = null;\n    watcher.run();\n    // in dev build, check and stop circular updates.\n    if (\"development\" !== 'production' && has[id] != null) {\n      circular[id] = (circular[id] || 0) + 1;\n      if (circular[id] > MAX_UPDATE_COUNT) {\n        warn(\n          'You may have an infinite update loop ' + (\n            watcher.user\n              ? (\"in watcher with expression \\\"\" + (watcher.expression) + \"\\\"\")\n              : \"in a component render function.\"\n          ),\n          watcher.vm\n        );\n        break\n      }\n    }\n  }\n\n  // keep copies of post queues before resetting state\n  var activatedQueue = activatedChildren.slice();\n  var updatedQueue = queue.slice();\n\n  resetSchedulerState();\n\n  // call component updated and activated hooks\n  callActivatedHooks(activatedQueue);\n  callUpdatedHooks(updatedQueue);\n\n  // devtool hook\n  /* istanbul ignore if */\n  if (devtools && config.devtools) {\n    devtools.emit('flush');\n  }\n}\n\nfunction callUpdatedHooks (queue) {\n  var i = queue.length;\n  while (i--) {\n    var watcher = queue[i];\n    var vm = watcher.vm;\n    if (vm._watcher === watcher && vm._isMounted) {\n      callHook(vm, 'updated');\n    }\n  }\n}\n\n/**\n * Queue a kept-alive component that was activated during patch.\n * The queue will be processed after the entire tree has been patched.\n */\nfunction queueActivatedComponent (vm) {\n  // setting _inactive to false here so that a render function can\n  // rely on checking whether it's in an inactive tree (e.g. router-view)\n  vm._inactive = false;\n  activatedChildren.push(vm);\n}\n\nfunction callActivatedHooks (queue) {\n  for (var i = 0; i < queue.length; i++) {\n    queue[i]._inactive = true;\n    activateChildComponent(queue[i], true /* true */);\n  }\n}\n\n/**\n * Push a watcher into the watcher queue.\n * Jobs with duplicate IDs will be skipped unless it's\n * pushed when the queue is being flushed.\n */\nfunction queueWatcher (watcher) {\n  var id = watcher.id;\n  if (has[id] == null) {\n    has[id] = true;\n    if (!flushing) {\n      queue.push(watcher);\n    } else {\n      // if already flushing, splice the watcher based on its id\n      // if already past its id, it will be run next immediately.\n      var i = queue.length - 1;\n      while (i > index && queue[i].id > watcher.id) {\n        i--;\n      }\n      queue.splice(i + 1, 0, watcher);\n    }\n    // queue the flush\n    if (!waiting) {\n      waiting = true;\n      nextTick(flushSchedulerQueue);\n    }\n  }\n}\n\n/*  */\n\nvar uid$2 = 0;\n\n/**\n * A watcher parses an expression, collects dependencies,\n * and fires callback when the expression value changes.\n * This is used for both the $watch() api and directives.\n */\nvar Watcher = function Watcher (\n  vm,\n  expOrFn,\n  cb,\n  options,\n  isRenderWatcher\n) {\n  this.vm = vm;\n  if (isRenderWatcher) {\n    vm._watcher = this;\n  }\n  vm._watchers.push(this);\n  // options\n  if (options) {\n    this.deep = !!options.deep;\n    this.user = !!options.user;\n    this.lazy = !!options.lazy;\n    this.sync = !!options.sync;\n  } else {\n    this.deep = this.user = this.lazy = this.sync = false;\n  }\n  this.cb = cb;\n  this.id = ++uid$2; // uid for batching\n  this.active = true;\n  this.dirty = this.lazy; // for lazy watchers\n  this.deps = [];\n  this.newDeps = [];\n  this.depIds = new _Set();\n  this.newDepIds = new _Set();\n  this.expression = expOrFn.toString();\n  // parse expression for getter\n  if (typeof expOrFn === 'function') {\n    this.getter = expOrFn;\n  } else {\n    this.getter = parsePath(expOrFn);\n    if (!this.getter) {\n      this.getter = function () {};\n      \"development\" !== 'production' && warn(\n        \"Failed watching path: \\\"\" + expOrFn + \"\\\" \" +\n        'Watcher only accepts simple dot-delimited paths. ' +\n        'For full control, use a function instead.',\n        vm\n      );\n    }\n  }\n  this.value = this.lazy\n    ? undefined\n    : this.get();\n};\n\n/**\n * Evaluate the getter, and re-collect dependencies.\n */\nWatcher.prototype.get = function get () {\n  pushTarget(this);\n  var value;\n  var vm = this.vm;\n  try {\n    value = this.getter.call(vm, vm);\n  } catch (e) {\n    if (this.user) {\n      handleError(e, vm, (\"getter for watcher \\\"\" + (this.expression) + \"\\\"\"));\n    } else {\n      throw e\n    }\n  } finally {\n    // \"touch\" every property so they are all tracked as\n    // dependencies for deep watching\n    if (this.deep) {\n      traverse(value);\n    }\n    popTarget();\n    this.cleanupDeps();\n  }\n  return value\n};\n\n/**\n * Add a dependency to this directive.\n */\nWatcher.prototype.addDep = function addDep (dep) {\n  var id = dep.id;\n  if (!this.newDepIds.has(id)) {\n    this.newDepIds.add(id);\n    this.newDeps.push(dep);\n    if (!this.depIds.has(id)) {\n      dep.addSub(this);\n    }\n  }\n};\n\n/**\n * Clean up for dependency collection.\n */\nWatcher.prototype.cleanupDeps = function cleanupDeps () {\n    var this$1 = this;\n\n  var i = this.deps.length;\n  while (i--) {\n    var dep = this$1.deps[i];\n    if (!this$1.newDepIds.has(dep.id)) {\n      dep.removeSub(this$1);\n    }\n  }\n  var tmp = this.depIds;\n  this.depIds = this.newDepIds;\n  this.newDepIds = tmp;\n  this.newDepIds.clear();\n  tmp = this.deps;\n  this.deps = this.newDeps;\n  this.newDeps = tmp;\n  this.newDeps.length = 0;\n};\n\n/**\n * Subscriber interface.\n * Will be called when a dependency changes.\n */\nWatcher.prototype.update = function update () {\n  /* istanbul ignore else */\n  if (this.lazy) {\n    this.dirty = true;\n  } else if (this.sync) {\n    this.run();\n  } else {\n    queueWatcher(this);\n  }\n};\n\n/**\n * Scheduler job interface.\n * Will be called by the scheduler.\n */\nWatcher.prototype.run = function run () {\n  if (this.active) {\n    var value = this.get();\n    if (\n      value !== this.value ||\n      // Deep watchers and watchers on Object/Arrays should fire even\n      // when the value is the same, because the value may\n      // have mutated.\n      isObject(value) ||\n      this.deep\n    ) {\n      // set new value\n      var oldValue = this.value;\n      this.value = value;\n      if (this.user) {\n        try {\n          this.cb.call(this.vm, value, oldValue);\n        } catch (e) {\n          handleError(e, this.vm, (\"callback for watcher \\\"\" + (this.expression) + \"\\\"\"));\n        }\n      } else {\n        this.cb.call(this.vm, value, oldValue);\n      }\n    }\n  }\n};\n\n/**\n * Evaluate the value of the watcher.\n * This only gets called for lazy watchers.\n */\nWatcher.prototype.evaluate = function evaluate () {\n  this.value = this.get();\n  this.dirty = false;\n};\n\n/**\n * Depend on all deps collected by this watcher.\n */\nWatcher.prototype.depend = function depend () {\n    var this$1 = this;\n\n  var i = this.deps.length;\n  while (i--) {\n    this$1.deps[i].depend();\n  }\n};\n\n/**\n * Remove self from all dependencies' subscriber list.\n */\nWatcher.prototype.teardown = function teardown () {\n    var this$1 = this;\n\n  if (this.active) {\n    // remove self from vm's watcher list\n    // this is a somewhat expensive operation so we skip it\n    // if the vm is being destroyed.\n    if (!this.vm._isBeingDestroyed) {\n      remove(this.vm._watchers, this);\n    }\n    var i = this.deps.length;\n    while (i--) {\n      this$1.deps[i].removeSub(this$1);\n    }\n    this.active = false;\n  }\n};\n\n/*  */\n\nvar sharedPropertyDefinition = {\n  enumerable: true,\n  configurable: true,\n  get: noop,\n  set: noop\n};\n\nfunction proxy (target, sourceKey, key) {\n  sharedPropertyDefinition.get = function proxyGetter () {\n    return this[sourceKey][key]\n  };\n  sharedPropertyDefinition.set = function proxySetter (val) {\n    this[sourceKey][key] = val;\n  };\n  Object.defineProperty(target, key, sharedPropertyDefinition);\n}\n\nfunction initState (vm) {\n  vm._watchers = [];\n  var opts = vm.$options;\n  if (opts.props) { initProps(vm, opts.props); }\n  if (opts.methods) { initMethods(vm, opts.methods); }\n  if (opts.data) {\n    initData(vm);\n  } else {\n    observe(vm._data = {}, true /* asRootData */);\n  }\n  if (opts.computed) { initComputed(vm, opts.computed); }\n  if (opts.watch && opts.watch !== nativeWatch) {\n    initWatch(vm, opts.watch);\n  }\n}\n\nfunction initProps (vm, propsOptions) {\n  var propsData = vm.$options.propsData || {};\n  var props = vm._props = {};\n  // cache prop keys so that future props updates can iterate using Array\n  // instead of dynamic object key enumeration.\n  var keys = vm.$options._propKeys = [];\n  var isRoot = !vm.$parent;\n  // root instance props should be converted\n  observerState.shouldConvert = isRoot;\n  var loop = function ( key ) {\n    keys.push(key);\n    var value = validateProp(key, propsOptions, propsData, vm);\n    /* istanbul ignore else */\n    {\n      var hyphenatedKey = hyphenate(key);\n      if (isReservedAttribute(hyphenatedKey) ||\n          config.isReservedAttr(hyphenatedKey)) {\n        warn(\n          (\"\\\"\" + hyphenatedKey + \"\\\" is a reserved attribute and cannot be used as component prop.\"),\n          vm\n        );\n      }\n      defineReactive(props, key, value, function () {\n        if (vm.$parent && !isUpdatingChildComponent) {\n          warn(\n            \"Avoid mutating a prop directly since the value will be \" +\n            \"overwritten whenever the parent component re-renders. \" +\n            \"Instead, use a data or computed property based on the prop's \" +\n            \"value. Prop being mutated: \\\"\" + key + \"\\\"\",\n            vm\n          );\n        }\n      });\n    }\n    // static props are already proxied on the component's prototype\n    // during Vue.extend(). We only need to proxy props defined at\n    // instantiation here.\n    if (!(key in vm)) {\n      proxy(vm, \"_props\", key);\n    }\n  };\n\n  for (var key in propsOptions) loop( key );\n  observerState.shouldConvert = true;\n}\n\nfunction initData (vm) {\n  var data = vm.$options.data;\n  data = vm._data = typeof data === 'function'\n    ? getData(data, vm)\n    : data || {};\n  if (!isPlainObject(data)) {\n    data = {};\n    \"development\" !== 'production' && warn(\n      'data functions should return an object:\\n' +\n      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',\n      vm\n    );\n  }\n  // proxy data on instance\n  var keys = Object.keys(data);\n  var props = vm.$options.props;\n  var methods = vm.$options.methods;\n  var i = keys.length;\n  while (i--) {\n    var key = keys[i];\n    {\n      if (methods && hasOwn(methods, key)) {\n        warn(\n          (\"Method \\\"\" + key + \"\\\" has already been defined as a data property.\"),\n          vm\n        );\n      }\n    }\n    if (props && hasOwn(props, key)) {\n      \"development\" !== 'production' && warn(\n        \"The data property \\\"\" + key + \"\\\" is already declared as a prop. \" +\n        \"Use prop default value instead.\",\n        vm\n      );\n    } else if (!isReserved(key)) {\n      proxy(vm, \"_data\", key);\n    }\n  }\n  // observe data\n  observe(data, true /* asRootData */);\n}\n\nfunction getData (data, vm) {\n  try {\n    return data.call(vm, vm)\n  } catch (e) {\n    handleError(e, vm, \"data()\");\n    return {}\n  }\n}\n\nvar computedWatcherOptions = { lazy: true };\n\nfunction initComputed (vm, computed) {\n  var watchers = vm._computedWatchers = Object.create(null);\n  // computed properties are just getters during SSR\n  var isSSR = isServerRendering();\n\n  for (var key in computed) {\n    var userDef = computed[key];\n    var getter = typeof userDef === 'function' ? userDef : userDef.get;\n    if (\"development\" !== 'production' && getter == null) {\n      warn(\n        (\"Getter is missing for computed property \\\"\" + key + \"\\\".\"),\n        vm\n      );\n    }\n\n    if (!isSSR) {\n      // create internal watcher for the computed property.\n      watchers[key] = new Watcher(\n        vm,\n        getter || noop,\n        noop,\n        computedWatcherOptions\n      );\n    }\n\n    // component-defined computed properties are already defined on the\n    // component prototype. We only need to define computed properties defined\n    // at instantiation here.\n    if (!(key in vm)) {\n      defineComputed(vm, key, userDef);\n    } else {\n      if (key in vm.$data) {\n        warn((\"The computed property \\\"\" + key + \"\\\" is already defined in data.\"), vm);\n      } else if (vm.$options.props && key in vm.$options.props) {\n        warn((\"The computed property \\\"\" + key + \"\\\" is already defined as a prop.\"), vm);\n      }\n    }\n  }\n}\n\nfunction defineComputed (\n  target,\n  key,\n  userDef\n) {\n  var shouldCache = !isServerRendering();\n  if (typeof userDef === 'function') {\n    sharedPropertyDefinition.get = shouldCache\n      ? createComputedGetter(key)\n      : userDef;\n    sharedPropertyDefinition.set = noop;\n  } else {\n    sharedPropertyDefinition.get = userDef.get\n      ? shouldCache && userDef.cache !== false\n        ? createComputedGetter(key)\n        : userDef.get\n      : noop;\n    sharedPropertyDefinition.set = userDef.set\n      ? userDef.set\n      : noop;\n  }\n  if (\"development\" !== 'production' &&\n      sharedPropertyDefinition.set === noop) {\n    sharedPropertyDefinition.set = function () {\n      warn(\n        (\"Computed property \\\"\" + key + \"\\\" was assigned to but it has no setter.\"),\n        this\n      );\n    };\n  }\n  Object.defineProperty(target, key, sharedPropertyDefinition);\n}\n\nfunction createComputedGetter (key) {\n  return function computedGetter () {\n    var watcher = this._computedWatchers && this._computedWatchers[key];\n    if (watcher) {\n      if (watcher.dirty) {\n        watcher.evaluate();\n      }\n      if (Dep.target) {\n        watcher.depend();\n      }\n      return watcher.value\n    }\n  }\n}\n\nfunction initMethods (vm, methods) {\n  var props = vm.$options.props;\n  for (var key in methods) {\n    {\n      if (methods[key] == null) {\n        warn(\n          \"Method \\\"\" + key + \"\\\" has an undefined value in the component definition. \" +\n          \"Did you reference the function correctly?\",\n          vm\n        );\n      }\n      if (props && hasOwn(props, key)) {\n        warn(\n          (\"Method \\\"\" + key + \"\\\" has already been defined as a prop.\"),\n          vm\n        );\n      }\n      if ((key in vm) && isReserved(key)) {\n        warn(\n          \"Method \\\"\" + key + \"\\\" conflicts with an existing Vue instance method. \" +\n          \"Avoid defining component methods that start with _ or $.\"\n        );\n      }\n    }\n    vm[key] = methods[key] == null ? noop : bind(methods[key], vm);\n  }\n}\n\nfunction initWatch (vm, watch) {\n  for (var key in watch) {\n    var handler = watch[key];\n    if (Array.isArray(handler)) {\n      for (var i = 0; i < handler.length; i++) {\n        createWatcher(vm, key, handler[i]);\n      }\n    } else {\n      createWatcher(vm, key, handler);\n    }\n  }\n}\n\nfunction createWatcher (\n  vm,\n  keyOrFn,\n  handler,\n  options\n) {\n  if (isPlainObject(handler)) {\n    options = handler;\n    handler = handler.handler;\n  }\n  if (typeof handler === 'string') {\n    handler = vm[handler];\n  }\n  return vm.$watch(keyOrFn, handler, options)\n}\n\nfunction stateMixin (Vue) {\n  // flow somehow has problems with directly declared definition object\n  // when using Object.defineProperty, so we have to procedurally build up\n  // the object here.\n  var dataDef = {};\n  dataDef.get = function () { return this._data };\n  var propsDef = {};\n  propsDef.get = function () { return this._props };\n  {\n    dataDef.set = function (newData) {\n      warn(\n        'Avoid replacing instance root $data. ' +\n        'Use nested data properties instead.',\n        this\n      );\n    };\n    propsDef.set = function () {\n      warn(\"$props is readonly.\", this);\n    };\n  }\n  Object.defineProperty(Vue.prototype, '$data', dataDef);\n  Object.defineProperty(Vue.prototype, '$props', propsDef);\n\n  Vue.prototype.$set = set;\n  Vue.prototype.$delete = del;\n\n  Vue.prototype.$watch = function (\n    expOrFn,\n    cb,\n    options\n  ) {\n    var vm = this;\n    if (isPlainObject(cb)) {\n      return createWatcher(vm, expOrFn, cb, options)\n    }\n    options = options || {};\n    options.user = true;\n    var watcher = new Watcher(vm, expOrFn, cb, options);\n    if (options.immediate) {\n      cb.call(vm, watcher.value);\n    }\n    return function unwatchFn () {\n      watcher.teardown();\n    }\n  };\n}\n\n/*  */\n\nfunction initProvide (vm) {\n  var provide = vm.$options.provide;\n  if (provide) {\n    vm._provided = typeof provide === 'function'\n      ? provide.call(vm)\n      : provide;\n  }\n}\n\nfunction initInjections (vm) {\n  var result = resolveInject(vm.$options.inject, vm);\n  if (result) {\n    observerState.shouldConvert = false;\n    Object.keys(result).forEach(function (key) {\n      /* istanbul ignore else */\n      {\n        defineReactive(vm, key, result[key], function () {\n          warn(\n            \"Avoid mutating an injected value directly since the changes will be \" +\n            \"overwritten whenever the provided component re-renders. \" +\n            \"injection being mutated: \\\"\" + key + \"\\\"\",\n            vm\n          );\n        });\n      }\n    });\n    observerState.shouldConvert = true;\n  }\n}\n\nfunction resolveInject (inject, vm) {\n  if (inject) {\n    // inject is :any because flow is not smart enough to figure out cached\n    var result = Object.create(null);\n    var keys = hasSymbol\n        ? Reflect.ownKeys(inject).filter(function (key) {\n          /* istanbul ignore next */\n          return Object.getOwnPropertyDescriptor(inject, key).enumerable\n        })\n        : Object.keys(inject);\n\n    for (var i = 0; i < keys.length; i++) {\n      var key = keys[i];\n      var provideKey = inject[key].from;\n      var source = vm;\n      while (source) {\n        if (source._provided && provideKey in source._provided) {\n          result[key] = source._provided[provideKey];\n          break\n        }\n        source = source.$parent;\n      }\n      if (!source) {\n        if ('default' in inject[key]) {\n          var provideDefault = inject[key].default;\n          result[key] = typeof provideDefault === 'function'\n            ? provideDefault.call(vm)\n            : provideDefault;\n        } else {\n          warn((\"Injection \\\"\" + key + \"\\\" not found\"), vm);\n        }\n      }\n    }\n    return result\n  }\n}\n\n/*  */\n\n/**\n * Runtime helper for rendering v-for lists.\n */\nfunction renderList (\n  val,\n  render\n) {\n  var ret, i, l, keys, key;\n  if (Array.isArray(val) || typeof val === 'string') {\n    ret = new Array(val.length);\n    for (i = 0, l = val.length; i < l; i++) {\n      ret[i] = render(val[i], i);\n    }\n  } else if (typeof val === 'number') {\n    ret = new Array(val);\n    for (i = 0; i < val; i++) {\n      ret[i] = render(i + 1, i);\n    }\n  } else if (isObject(val)) {\n    keys = Object.keys(val);\n    ret = new Array(keys.length);\n    for (i = 0, l = keys.length; i < l; i++) {\n      key = keys[i];\n      ret[i] = render(val[key], key, i);\n    }\n  }\n  if (isDef(ret)) {\n    (ret)._isVList = true;\n  }\n  return ret\n}\n\n/*  */\n\n/**\n * Runtime helper for rendering <slot>\n */\nfunction renderSlot (\n  name,\n  fallback,\n  props,\n  bindObject\n) {\n  var scopedSlotFn = this.$scopedSlots[name];\n  var nodes;\n  if (scopedSlotFn) { // scoped slot\n    props = props || {};\n    if (bindObject) {\n      if (\"development\" !== 'production' && !isObject(bindObject)) {\n        warn(\n          'slot v-bind without argument expects an Object',\n          this\n        );\n      }\n      props = extend(extend({}, bindObject), props);\n    }\n    nodes = scopedSlotFn(props) || fallback;\n  } else {\n    var slotNodes = this.$slots[name];\n    // warn duplicate slot usage\n    if (slotNodes) {\n      if (\"development\" !== 'production' && slotNodes._rendered) {\n        warn(\n          \"Duplicate presence of slot \\\"\" + name + \"\\\" found in the same render tree \" +\n          \"- this will likely cause render errors.\",\n          this\n        );\n      }\n      slotNodes._rendered = true;\n    }\n    nodes = slotNodes || fallback;\n  }\n\n  var target = props && props.slot;\n  if (target) {\n    return this.$createElement('template', { slot: target }, nodes)\n  } else {\n    return nodes\n  }\n}\n\n/*  */\n\n/**\n * Runtime helper for resolving filters\n */\nfunction resolveFilter (id) {\n  return resolveAsset(this.$options, 'filters', id, true) || identity\n}\n\n/*  */\n\n/**\n * Runtime helper for checking keyCodes from config.\n * exposed as Vue.prototype._k\n * passing in eventKeyName as last argument separately for backwards compat\n */\nfunction checkKeyCodes (\n  eventKeyCode,\n  key,\n  builtInAlias,\n  eventKeyName\n) {\n  var keyCodes = config.keyCodes[key] || builtInAlias;\n  if (keyCodes) {\n    if (Array.isArray(keyCodes)) {\n      return keyCodes.indexOf(eventKeyCode) === -1\n    } else {\n      return keyCodes !== eventKeyCode\n    }\n  } else if (eventKeyName) {\n    return hyphenate(eventKeyName) !== key\n  }\n}\n\n/*  */\n\n/**\n * Runtime helper for merging v-bind=\"object\" into a VNode's data.\n */\nfunction bindObjectProps (\n  data,\n  tag,\n  value,\n  asProp,\n  isSync\n) {\n  if (value) {\n    if (!isObject(value)) {\n      \"development\" !== 'production' && warn(\n        'v-bind without argument expects an Object or Array value',\n        this\n      );\n    } else {\n      if (Array.isArray(value)) {\n        value = toObject(value);\n      }\n      var hash;\n      var loop = function ( key ) {\n        if (\n          key === 'class' ||\n          key === 'style' ||\n          isReservedAttribute(key)\n        ) {\n          hash = data;\n        } else {\n          var type = data.attrs && data.attrs.type;\n          hash = asProp || config.mustUseProp(tag, type, key)\n            ? data.domProps || (data.domProps = {})\n            : data.attrs || (data.attrs = {});\n        }\n        if (!(key in hash)) {\n          hash[key] = value[key];\n\n          if (isSync) {\n            var on = data.on || (data.on = {});\n            on[(\"update:\" + key)] = function ($event) {\n              value[key] = $event;\n            };\n          }\n        }\n      };\n\n      for (var key in value) loop( key );\n    }\n  }\n  return data\n}\n\n/*  */\n\n/**\n * Runtime helper for rendering static trees.\n */\nfunction renderStatic (\n  index,\n  isInFor\n) {\n  var cached = this._staticTrees || (this._staticTrees = []);\n  var tree = cached[index];\n  // if has already-rendered static tree and not inside v-for,\n  // we can reuse the same tree by doing a shallow clone.\n  if (tree && !isInFor) {\n    return Array.isArray(tree)\n      ? cloneVNodes(tree)\n      : cloneVNode(tree)\n  }\n  // otherwise, render a fresh tree.\n  tree = cached[index] = this.$options.staticRenderFns[index].call(\n    this._renderProxy,\n    null,\n    this // for render fns generated for functional component templates\n  );\n  markStatic(tree, (\"__static__\" + index), false);\n  return tree\n}\n\n/**\n * Runtime helper for v-once.\n * Effectively it means marking the node as static with a unique key.\n */\nfunction markOnce (\n  tree,\n  index,\n  key\n) {\n  markStatic(tree, (\"__once__\" + index + (key ? (\"_\" + key) : \"\")), true);\n  return tree\n}\n\nfunction markStatic (\n  tree,\n  key,\n  isOnce\n) {\n  if (Array.isArray(tree)) {\n    for (var i = 0; i < tree.length; i++) {\n      if (tree[i] && typeof tree[i] !== 'string') {\n        markStaticNode(tree[i], (key + \"_\" + i), isOnce);\n      }\n    }\n  } else {\n    markStaticNode(tree, key, isOnce);\n  }\n}\n\nfunction markStaticNode (node, key, isOnce) {\n  node.isStatic = true;\n  node.key = key;\n  node.isOnce = isOnce;\n}\n\n/*  */\n\nfunction bindObjectListeners (data, value) {\n  if (value) {\n    if (!isPlainObject(value)) {\n      \"development\" !== 'production' && warn(\n        'v-on without argument expects an Object value',\n        this\n      );\n    } else {\n      var on = data.on = data.on ? extend({}, data.on) : {};\n      for (var key in value) {\n        var existing = on[key];\n        var ours = value[key];\n        on[key] = existing ? [].concat(existing, ours) : ours;\n      }\n    }\n  }\n  return data\n}\n\n/*  */\n\nfunction installRenderHelpers (target) {\n  target._o = markOnce;\n  target._n = toNumber;\n  target._s = toString;\n  target._l = renderList;\n  target._t = renderSlot;\n  target._q = looseEqual;\n  target._i = looseIndexOf;\n  target._m = renderStatic;\n  target._f = resolveFilter;\n  target._k = checkKeyCodes;\n  target._b = bindObjectProps;\n  target._v = createTextVNode;\n  target._e = createEmptyVNode;\n  target._u = resolveScopedSlots;\n  target._g = bindObjectListeners;\n}\n\n/*  */\n\nfunction FunctionalRenderContext (\n  data,\n  props,\n  children,\n  parent,\n  Ctor\n) {\n  var options = Ctor.options;\n  this.data = data;\n  this.props = props;\n  this.children = children;\n  this.parent = parent;\n  this.listeners = data.on || emptyObject;\n  this.injections = resolveInject(options.inject, parent);\n  this.slots = function () { return resolveSlots(children, parent); };\n\n  // ensure the createElement function in functional components\n  // gets a unique context - this is necessary for correct named slot check\n  var contextVm = Object.create(parent);\n  var isCompiled = isTrue(options._compiled);\n  var needNormalization = !isCompiled;\n\n  // support for compiled functional template\n  if (isCompiled) {\n    // exposing $options for renderStatic()\n    this.$options = options;\n    // pre-resolve slots for renderSlot()\n    this.$slots = this.slots();\n    this.$scopedSlots = data.scopedSlots || emptyObject;\n  }\n\n  if (options._scopeId) {\n    this._c = function (a, b, c, d) {\n      var vnode = createElement(contextVm, a, b, c, d, needNormalization);\n      if (vnode) {\n        vnode.fnScopeId = options._scopeId;\n        vnode.fnContext = parent;\n      }\n      return vnode\n    };\n  } else {\n    this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };\n  }\n}\n\ninstallRenderHelpers(FunctionalRenderContext.prototype);\n\nfunction createFunctionalComponent (\n  Ctor,\n  propsData,\n  data,\n  contextVm,\n  children\n) {\n  var options = Ctor.options;\n  var props = {};\n  var propOptions = options.props;\n  if (isDef(propOptions)) {\n    for (var key in propOptions) {\n      props[key] = validateProp(key, propOptions, propsData || emptyObject);\n    }\n  } else {\n    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }\n    if (isDef(data.props)) { mergeProps(props, data.props); }\n  }\n\n  var renderContext = new FunctionalRenderContext(\n    data,\n    props,\n    children,\n    contextVm,\n    Ctor\n  );\n\n  var vnode = options.render.call(null, renderContext._c, renderContext);\n\n  if (vnode instanceof VNode) {\n    vnode.fnContext = contextVm;\n    vnode.fnOptions = options;\n    if (data.slot) {\n      (vnode.data || (vnode.data = {})).slot = data.slot;\n    }\n  }\n\n  return vnode\n}\n\nfunction mergeProps (to, from) {\n  for (var key in from) {\n    to[camelize(key)] = from[key];\n  }\n}\n\n/*  */\n\n// hooks to be invoked on component VNodes during patch\nvar componentVNodeHooks = {\n  init: function init (\n    vnode,\n    hydrating,\n    parentElm,\n    refElm\n  ) {\n    if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {\n      var child = vnode.componentInstance = createComponentInstanceForVnode(\n        vnode,\n        activeInstance,\n        parentElm,\n        refElm\n      );\n      child.$mount(hydrating ? vnode.elm : undefined, hydrating);\n    } else if (vnode.data.keepAlive) {\n      // kept-alive components, treat as a patch\n      var mountedNode = vnode; // work around flow\n      componentVNodeHooks.prepatch(mountedNode, mountedNode);\n    }\n  },\n\n  prepatch: function prepatch (oldVnode, vnode) {\n    var options = vnode.componentOptions;\n    var child = vnode.componentInstance = oldVnode.componentInstance;\n    updateChildComponent(\n      child,\n      options.propsData, // updated props\n      options.listeners, // updated listeners\n      vnode, // new parent vnode\n      options.children // new children\n    );\n  },\n\n  insert: function insert (vnode) {\n    var context = vnode.context;\n    var componentInstance = vnode.componentInstance;\n    if (!componentInstance._isMounted) {\n      componentInstance._isMounted = true;\n      callHook(componentInstance, 'mounted');\n    }\n    if (vnode.data.keepAlive) {\n      if (context._isMounted) {\n        // vue-router#1212\n        // During updates, a kept-alive component's child components may\n        // change, so directly walking the tree here may call activated hooks\n        // on incorrect children. Instead we push them into a queue which will\n        // be processed after the whole patch process ended.\n        queueActivatedComponent(componentInstance);\n      } else {\n        activateChildComponent(componentInstance, true /* direct */);\n      }\n    }\n  },\n\n  destroy: function destroy (vnode) {\n    var componentInstance = vnode.componentInstance;\n    if (!componentInstance._isDestroyed) {\n      if (!vnode.data.keepAlive) {\n        componentInstance.$destroy();\n      } else {\n        deactivateChildComponent(componentInstance, true /* direct */);\n      }\n    }\n  }\n};\n\nvar hooksToMerge = Object.keys(componentVNodeHooks);\n\nfunction createComponent (\n  Ctor,\n  data,\n  context,\n  children,\n  tag\n) {\n  if (isUndef(Ctor)) {\n    return\n  }\n\n  var baseCtor = context.$options._base;\n\n  // plain options object: turn it into a constructor\n  if (isObject(Ctor)) {\n    Ctor = baseCtor.extend(Ctor);\n  }\n\n  // if at this stage it's not a constructor or an async component factory,\n  // reject.\n  if (typeof Ctor !== 'function') {\n    {\n      warn((\"Invalid Component definition: \" + (String(Ctor))), context);\n    }\n    return\n  }\n\n  // async component\n  var asyncFactory;\n  if (isUndef(Ctor.cid)) {\n    asyncFactory = Ctor;\n    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);\n    if (Ctor === undefined) {\n      // return a placeholder node for async component, which is rendered\n      // as a comment node but preserves all the raw information for the node.\n      // the information will be used for async server-rendering and hydration.\n      return createAsyncPlaceholder(\n        asyncFactory,\n        data,\n        context,\n        children,\n        tag\n      )\n    }\n  }\n\n  data = data || {};\n\n  // resolve constructor options in case global mixins are applied after\n  // component constructor creation\n  resolveConstructorOptions(Ctor);\n\n  // transform component v-model data into props & events\n  if (isDef(data.model)) {\n    transformModel(Ctor.options, data);\n  }\n\n  // extract props\n  var propsData = extractPropsFromVNodeData(data, Ctor, tag);\n\n  // functional component\n  if (isTrue(Ctor.options.functional)) {\n    return createFunctionalComponent(Ctor, propsData, data, context, children)\n  }\n\n  // extract listeners, since these needs to be treated as\n  // child component listeners instead of DOM listeners\n  var listeners = data.on;\n  // replace with listeners with .native modifier\n  // so it gets processed during parent component patch.\n  data.on = data.nativeOn;\n\n  if (isTrue(Ctor.options.abstract)) {\n    // abstract components do not keep anything\n    // other than props & listeners & slot\n\n    // work around flow\n    var slot = data.slot;\n    data = {};\n    if (slot) {\n      data.slot = slot;\n    }\n  }\n\n  // merge component management hooks onto the placeholder node\n  mergeHooks(data);\n\n  // return a placeholder vnode\n  var name = Ctor.options.name || tag;\n  var vnode = new VNode(\n    (\"vue-component-\" + (Ctor.cid) + (name ? (\"-\" + name) : '')),\n    data, undefined, undefined, undefined, context,\n    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },\n    asyncFactory\n  );\n  return vnode\n}\n\nfunction createComponentInstanceForVnode (\n  vnode, // we know it's MountedComponentVNode but flow doesn't\n  parent, // activeInstance in lifecycle state\n  parentElm,\n  refElm\n) {\n  var options = {\n    _isComponent: true,\n    parent: parent,\n    _parentVnode: vnode,\n    _parentElm: parentElm || null,\n    _refElm: refElm || null\n  };\n  // check inline-template render functions\n  var inlineTemplate = vnode.data.inlineTemplate;\n  if (isDef(inlineTemplate)) {\n    options.render = inlineTemplate.render;\n    options.staticRenderFns = inlineTemplate.staticRenderFns;\n  }\n  return new vnode.componentOptions.Ctor(options)\n}\n\nfunction mergeHooks (data) {\n  if (!data.hook) {\n    data.hook = {};\n  }\n  for (var i = 0; i < hooksToMerge.length; i++) {\n    var key = hooksToMerge[i];\n    var fromParent = data.hook[key];\n    var ours = componentVNodeHooks[key];\n    data.hook[key] = fromParent ? mergeHook$1(ours, fromParent) : ours;\n  }\n}\n\nfunction mergeHook$1 (one, two) {\n  return function (a, b, c, d) {\n    one(a, b, c, d);\n    two(a, b, c, d);\n  }\n}\n\n// transform component v-model info (value and callback) into\n// prop and event handler respectively.\nfunction transformModel (options, data) {\n  var prop = (options.model && options.model.prop) || 'value';\n  var event = (options.model && options.model.event) || 'input';(data.props || (data.props = {}))[prop] = data.model.value;\n  var on = data.on || (data.on = {});\n  if (isDef(on[event])) {\n    on[event] = [data.model.callback].concat(on[event]);\n  } else {\n    on[event] = data.model.callback;\n  }\n}\n\n/*  */\n\nvar SIMPLE_NORMALIZE = 1;\nvar ALWAYS_NORMALIZE = 2;\n\n// wrapper function for providing a more flexible interface\n// without getting yelled at by flow\nfunction createElement (\n  context,\n  tag,\n  data,\n  children,\n  normalizationType,\n  alwaysNormalize\n) {\n  if (Array.isArray(data) || isPrimitive(data)) {\n    normalizationType = children;\n    children = data;\n    data = undefined;\n  }\n  if (isTrue(alwaysNormalize)) {\n    normalizationType = ALWAYS_NORMALIZE;\n  }\n  return _createElement(context, tag, data, children, normalizationType)\n}\n\nfunction _createElement (\n  context,\n  tag,\n  data,\n  children,\n  normalizationType\n) {\n  if (isDef(data) && isDef((data).__ob__)) {\n    \"development\" !== 'production' && warn(\n      \"Avoid using observed data object as vnode data: \" + (JSON.stringify(data)) + \"\\n\" +\n      'Always create fresh vnode data objects in each render!',\n      context\n    );\n    return createEmptyVNode()\n  }\n  // object syntax in v-bind\n  if (isDef(data) && isDef(data.is)) {\n    tag = data.is;\n  }\n  if (!tag) {\n    // in case of component :is set to falsy value\n    return createEmptyVNode()\n  }\n  // warn against non-primitive key\n  if (\"development\" !== 'production' &&\n    isDef(data) && isDef(data.key) && !isPrimitive(data.key)\n  ) {\n    warn(\n      'Avoid using non-primitive value as key, ' +\n      'use string/number value instead.',\n      context\n    );\n  }\n  // support single function children as default scoped slot\n  if (Array.isArray(children) &&\n    typeof children[0] === 'function'\n  ) {\n    data = data || {};\n    data.scopedSlots = { default: children[0] };\n    children.length = 0;\n  }\n  if (normalizationType === ALWAYS_NORMALIZE) {\n    children = normalizeChildren(children);\n  } else if (normalizationType === SIMPLE_NORMALIZE) {\n    children = simpleNormalizeChildren(children);\n  }\n  var vnode, ns;\n  if (typeof tag === 'string') {\n    var Ctor;\n    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);\n    if (config.isReservedTag(tag)) {\n      // platform built-in elements\n      vnode = new VNode(\n        config.parsePlatformTagName(tag), data, children,\n        undefined, undefined, context\n      );\n    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {\n      // component\n      vnode = createComponent(Ctor, data, context, children, tag);\n    } else {\n      // unknown or unlisted namespaced elements\n      // check at runtime because it may get assigned a namespace when its\n      // parent normalizes children\n      vnode = new VNode(\n        tag, data, children,\n        undefined, undefined, context\n      );\n    }\n  } else {\n    // direct component options / constructor\n    vnode = createComponent(tag, data, context, children);\n  }\n  if (isDef(vnode)) {\n    if (ns) { applyNS(vnode, ns); }\n    return vnode\n  } else {\n    return createEmptyVNode()\n  }\n}\n\nfunction applyNS (vnode, ns, force) {\n  vnode.ns = ns;\n  if (vnode.tag === 'foreignObject') {\n    // use default namespace inside foreignObject\n    ns = undefined;\n    force = true;\n  }\n  if (isDef(vnode.children)) {\n    for (var i = 0, l = vnode.children.length; i < l; i++) {\n      var child = vnode.children[i];\n      if (isDef(child.tag) && (isUndef(child.ns) || isTrue(force))) {\n        applyNS(child, ns, force);\n      }\n    }\n  }\n}\n\n/*  */\n\nfunction initRender (vm) {\n  vm._vnode = null; // the root of the child tree\n  vm._staticTrees = null; // v-once cached trees\n  var options = vm.$options;\n  var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree\n  var renderContext = parentVnode && parentVnode.context;\n  vm.$slots = resolveSlots(options._renderChildren, renderContext);\n  vm.$scopedSlots = emptyObject;\n  // bind the createElement fn to this instance\n  // so that we get proper render context inside it.\n  // args order: tag, data, children, normalizationType, alwaysNormalize\n  // internal version is used by render functions compiled from templates\n  vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };\n  // normalization is always applied for the public version, used in\n  // user-written render functions.\n  vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };\n\n  // $attrs & $listeners are exposed for easier HOC creation.\n  // they need to be reactive so that HOCs using them are always updated\n  var parentData = parentVnode && parentVnode.data;\n\n  /* istanbul ignore else */\n  {\n    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {\n      !isUpdatingChildComponent && warn(\"$attrs is readonly.\", vm);\n    }, true);\n    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, function () {\n      !isUpdatingChildComponent && warn(\"$listeners is readonly.\", vm);\n    }, true);\n  }\n}\n\nfunction renderMixin (Vue) {\n  // install runtime convenience helpers\n  installRenderHelpers(Vue.prototype);\n\n  Vue.prototype.$nextTick = function (fn) {\n    return nextTick(fn, this)\n  };\n\n  Vue.prototype._render = function () {\n    var vm = this;\n    var ref = vm.$options;\n    var render = ref.render;\n    var _parentVnode = ref._parentVnode;\n\n    if (vm._isMounted) {\n      // if the parent didn't update, the slot nodes will be the ones from\n      // last render. They need to be cloned to ensure \"freshness\" for this render.\n      for (var key in vm.$slots) {\n        var slot = vm.$slots[key];\n        // _rendered is a flag added by renderSlot, but may not be present\n        // if the slot is passed from manually written render functions\n        if (slot._rendered || (slot[0] && slot[0].elm)) {\n          vm.$slots[key] = cloneVNodes(slot, true /* deep */);\n        }\n      }\n    }\n\n    vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject;\n\n    // set parent vnode. this allows render functions to have access\n    // to the data on the placeholder node.\n    vm.$vnode = _parentVnode;\n    // render self\n    var vnode;\n    try {\n      vnode = render.call(vm._renderProxy, vm.$createElement);\n    } catch (e) {\n      handleError(e, vm, \"render\");\n      // return error render result,\n      // or previous vnode to prevent render error causing blank component\n      /* istanbul ignore else */\n      {\n        if (vm.$options.renderError) {\n          try {\n            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);\n          } catch (e) {\n            handleError(e, vm, \"renderError\");\n            vnode = vm._vnode;\n          }\n        } else {\n          vnode = vm._vnode;\n        }\n      }\n    }\n    // return empty vnode in case the render function errored out\n    if (!(vnode instanceof VNode)) {\n      if (\"development\" !== 'production' && Array.isArray(vnode)) {\n        warn(\n          'Multiple root nodes returned from render function. Render function ' +\n          'should return a single root node.',\n          vm\n        );\n      }\n      vnode = createEmptyVNode();\n    }\n    // set parent\n    vnode.parent = _parentVnode;\n    return vnode\n  };\n}\n\n/*  */\n\nvar uid$1 = 0;\n\nfunction initMixin (Vue) {\n  Vue.prototype._init = function (options) {\n    var vm = this;\n    // a uid\n    vm._uid = uid$1++;\n\n    var startTag, endTag;\n    /* istanbul ignore if */\n    if (\"development\" !== 'production' && config.performance && mark) {\n      startTag = \"vue-perf-start:\" + (vm._uid);\n      endTag = \"vue-perf-end:\" + (vm._uid);\n      mark(startTag);\n    }\n\n    // a flag to avoid this being observed\n    vm._isVue = true;\n    // merge options\n    if (options && options._isComponent) {\n      // optimize internal component instantiation\n      // since dynamic options merging is pretty slow, and none of the\n      // internal component options needs special treatment.\n      initInternalComponent(vm, options);\n    } else {\n      vm.$options = mergeOptions(\n        resolveConstructorOptions(vm.constructor),\n        options || {},\n        vm\n      );\n    }\n    /* istanbul ignore else */\n    {\n      initProxy(vm);\n    }\n    // expose real self\n    vm._self = vm;\n    initLifecycle(vm);\n    initEvents(vm);\n    initRender(vm);\n    callHook(vm, 'beforeCreate');\n    initInjections(vm); // resolve injections before data/props\n    initState(vm);\n    initProvide(vm); // resolve provide after data/props\n    callHook(vm, 'created');\n\n    /* istanbul ignore if */\n    if (\"development\" !== 'production' && config.performance && mark) {\n      vm._name = formatComponentName(vm, false);\n      mark(endTag);\n      measure((\"vue \" + (vm._name) + \" init\"), startTag, endTag);\n    }\n\n    if (vm.$options.el) {\n      vm.$mount(vm.$options.el);\n    }\n  };\n}\n\nfunction initInternalComponent (vm, options) {\n  var opts = vm.$options = Object.create(vm.constructor.options);\n  // doing this because it's faster than dynamic enumeration.\n  var parentVnode = options._parentVnode;\n  opts.parent = options.parent;\n  opts._parentVnode = parentVnode;\n  opts._parentElm = options._parentElm;\n  opts._refElm = options._refElm;\n\n  var vnodeComponentOptions = parentVnode.componentOptions;\n  opts.propsData = vnodeComponentOptions.propsData;\n  opts._parentListeners = vnodeComponentOptions.listeners;\n  opts._renderChildren = vnodeComponentOptions.children;\n  opts._componentTag = vnodeComponentOptions.tag;\n\n  if (options.render) {\n    opts.render = options.render;\n    opts.staticRenderFns = options.staticRenderFns;\n  }\n}\n\nfunction resolveConstructorOptions (Ctor) {\n  var options = Ctor.options;\n  if (Ctor.super) {\n    var superOptions = resolveConstructorOptions(Ctor.super);\n    var cachedSuperOptions = Ctor.superOptions;\n    if (superOptions !== cachedSuperOptions) {\n      // super option changed,\n      // need to resolve new options.\n      Ctor.superOptions = superOptions;\n      // check if there are any late-modified/attached options (#4976)\n      var modifiedOptions = resolveModifiedOptions(Ctor);\n      // update base extend options\n      if (modifiedOptions) {\n        extend(Ctor.extendOptions, modifiedOptions);\n      }\n      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);\n      if (options.name) {\n        options.components[options.name] = Ctor;\n      }\n    }\n  }\n  return options\n}\n\nfunction resolveModifiedOptions (Ctor) {\n  var modified;\n  var latest = Ctor.options;\n  var extended = Ctor.extendOptions;\n  var sealed = Ctor.sealedOptions;\n  for (var key in latest) {\n    if (latest[key] !== sealed[key]) {\n      if (!modified) { modified = {}; }\n      modified[key] = dedupe(latest[key], extended[key], sealed[key]);\n    }\n  }\n  return modified\n}\n\nfunction dedupe (latest, extended, sealed) {\n  // compare latest and sealed to ensure lifecycle hooks won't be duplicated\n  // between merges\n  if (Array.isArray(latest)) {\n    var res = [];\n    sealed = Array.isArray(sealed) ? sealed : [sealed];\n    extended = Array.isArray(extended) ? extended : [extended];\n    for (var i = 0; i < latest.length; i++) {\n      // push original options and not sealed options to exclude duplicated options\n      if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {\n        res.push(latest[i]);\n      }\n    }\n    return res\n  } else {\n    return latest\n  }\n}\n\nfunction Vue$3 (options) {\n  if (\"development\" !== 'production' &&\n    !(this instanceof Vue$3)\n  ) {\n    warn('Vue is a constructor and should be called with the `new` keyword');\n  }\n  this._init(options);\n}\n\ninitMixin(Vue$3);\nstateMixin(Vue$3);\neventsMixin(Vue$3);\nlifecycleMixin(Vue$3);\nrenderMixin(Vue$3);\n\n/*  */\n\nfunction initUse (Vue) {\n  Vue.use = function (plugin) {\n    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));\n    if (installedPlugins.indexOf(plugin) > -1) {\n      return this\n    }\n\n    // additional parameters\n    var args = toArray(arguments, 1);\n    args.unshift(this);\n    if (typeof plugin.install === 'function') {\n      plugin.install.apply(plugin, args);\n    } else if (typeof plugin === 'function') {\n      plugin.apply(null, args);\n    }\n    installedPlugins.push(plugin);\n    return this\n  };\n}\n\n/*  */\n\nfunction initMixin$1 (Vue) {\n  Vue.mixin = function (mixin) {\n    this.options = mergeOptions(this.options, mixin);\n    return this\n  };\n}\n\n/*  */\n\nfunction initExtend (Vue) {\n  /**\n   * Each instance constructor, including Vue, has a unique\n   * cid. This enables us to create wrapped \"child\n   * constructors\" for prototypal inheritance and cache them.\n   */\n  Vue.cid = 0;\n  var cid = 1;\n\n  /**\n   * Class inheritance\n   */\n  Vue.extend = function (extendOptions) {\n    extendOptions = extendOptions || {};\n    var Super = this;\n    var SuperId = Super.cid;\n    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});\n    if (cachedCtors[SuperId]) {\n      return cachedCtors[SuperId]\n    }\n\n    var name = extendOptions.name || Super.options.name;\n    if (\"development\" !== 'production' && name) {\n      validateComponentName(name);\n    }\n\n    var Sub = function VueComponent (options) {\n      this._init(options);\n    };\n    Sub.prototype = Object.create(Super.prototype);\n    Sub.prototype.constructor = Sub;\n    Sub.cid = cid++;\n    Sub.options = mergeOptions(\n      Super.options,\n      extendOptions\n    );\n    Sub['super'] = Super;\n\n    // For props and computed properties, we define the proxy getters on\n    // the Vue instances at extension time, on the extended prototype. This\n    // avoids Object.defineProperty calls for each instance created.\n    if (Sub.options.props) {\n      initProps$1(Sub);\n    }\n    if (Sub.options.computed) {\n      initComputed$1(Sub);\n    }\n\n    // allow further extension/mixin/plugin usage\n    Sub.extend = Super.extend;\n    Sub.mixin = Super.mixin;\n    Sub.use = Super.use;\n\n    // create asset registers, so extended classes\n    // can have their private assets too.\n    ASSET_TYPES.forEach(function (type) {\n      Sub[type] = Super[type];\n    });\n    // enable recursive self-lookup\n    if (name) {\n      Sub.options.components[name] = Sub;\n    }\n\n    // keep a reference to the super options at extension time.\n    // later at instantiation we can check if Super's options have\n    // been updated.\n    Sub.superOptions = Super.options;\n    Sub.extendOptions = extendOptions;\n    Sub.sealedOptions = extend({}, Sub.options);\n\n    // cache constructor\n    cachedCtors[SuperId] = Sub;\n    return Sub\n  };\n}\n\nfunction initProps$1 (Comp) {\n  var props = Comp.options.props;\n  for (var key in props) {\n    proxy(Comp.prototype, \"_props\", key);\n  }\n}\n\nfunction initComputed$1 (Comp) {\n  var computed = Comp.options.computed;\n  for (var key in computed) {\n    defineComputed(Comp.prototype, key, computed[key]);\n  }\n}\n\n/*  */\n\nfunction initAssetRegisters (Vue) {\n  /**\n   * Create asset registration methods.\n   */\n  ASSET_TYPES.forEach(function (type) {\n    Vue[type] = function (\n      id,\n      definition\n    ) {\n      if (!definition) {\n        return this.options[type + 's'][id]\n      } else {\n        /* istanbul ignore if */\n        if (\"development\" !== 'production' && type === 'component') {\n          validateComponentName(id);\n        }\n        if (type === 'component' && isPlainObject(definition)) {\n          definition.name = definition.name || id;\n          definition = this.options._base.extend(definition);\n        }\n        if (type === 'directive' && typeof definition === 'function') {\n          definition = { bind: definition, update: definition };\n        }\n        this.options[type + 's'][id] = definition;\n        return definition\n      }\n    };\n  });\n}\n\n/*  */\n\nfunction getComponentName (opts) {\n  return opts && (opts.Ctor.options.name || opts.tag)\n}\n\nfunction matches (pattern, name) {\n  if (Array.isArray(pattern)) {\n    return pattern.indexOf(name) > -1\n  } else if (typeof pattern === 'string') {\n    return pattern.split(',').indexOf(name) > -1\n  } else if (isRegExp(pattern)) {\n    return pattern.test(name)\n  }\n  /* istanbul ignore next */\n  return false\n}\n\nfunction pruneCache (keepAliveInstance, filter) {\n  var cache = keepAliveInstance.cache;\n  var keys = keepAliveInstance.keys;\n  var _vnode = keepAliveInstance._vnode;\n  for (var key in cache) {\n    var cachedNode = cache[key];\n    if (cachedNode) {\n      var name = getComponentName(cachedNode.componentOptions);\n      if (name && !filter(name)) {\n        pruneCacheEntry(cache, key, keys, _vnode);\n      }\n    }\n  }\n}\n\nfunction pruneCacheEntry (\n  cache,\n  key,\n  keys,\n  current\n) {\n  var cached$$1 = cache[key];\n  if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {\n    cached$$1.componentInstance.$destroy();\n  }\n  cache[key] = null;\n  remove(keys, key);\n}\n\nvar patternTypes = [String, RegExp, Array];\n\nvar KeepAlive = {\n  name: 'keep-alive',\n  abstract: true,\n\n  props: {\n    include: patternTypes,\n    exclude: patternTypes,\n    max: [String, Number]\n  },\n\n  created: function created () {\n    this.cache = Object.create(null);\n    this.keys = [];\n  },\n\n  destroyed: function destroyed () {\n    var this$1 = this;\n\n    for (var key in this$1.cache) {\n      pruneCacheEntry(this$1.cache, key, this$1.keys);\n    }\n  },\n\n  watch: {\n    include: function include (val) {\n      pruneCache(this, function (name) { return matches(val, name); });\n    },\n    exclude: function exclude (val) {\n      pruneCache(this, function (name) { return !matches(val, name); });\n    }\n  },\n\n  render: function render () {\n    var slot = this.$slots.default;\n    var vnode = getFirstComponentChild(slot);\n    var componentOptions = vnode && vnode.componentOptions;\n    if (componentOptions) {\n      // check pattern\n      var name = getComponentName(componentOptions);\n      var ref = this;\n      var include = ref.include;\n      var exclude = ref.exclude;\n      if (\n        // not included\n        (include && (!name || !matches(include, name))) ||\n        // excluded\n        (exclude && name && matches(exclude, name))\n      ) {\n        return vnode\n      }\n\n      var ref$1 = this;\n      var cache = ref$1.cache;\n      var keys = ref$1.keys;\n      var key = vnode.key == null\n        // same constructor may get registered as different local components\n        // so cid alone is not enough (#3269)\n        ? componentOptions.Ctor.cid + (componentOptions.tag ? (\"::\" + (componentOptions.tag)) : '')\n        : vnode.key;\n      if (cache[key]) {\n        vnode.componentInstance = cache[key].componentInstance;\n        // make current key freshest\n        remove(keys, key);\n        keys.push(key);\n      } else {\n        cache[key] = vnode;\n        keys.push(key);\n        // prune oldest entry\n        if (this.max && keys.length > parseInt(this.max)) {\n          pruneCacheEntry(cache, keys[0], keys, this._vnode);\n        }\n      }\n\n      vnode.data.keepAlive = true;\n    }\n    return vnode || (slot && slot[0])\n  }\n};\n\nvar builtInComponents = {\n  KeepAlive: KeepAlive\n};\n\n/*  */\n\nfunction initGlobalAPI (Vue) {\n  // config\n  var configDef = {};\n  configDef.get = function () { return config; };\n  {\n    configDef.set = function () {\n      warn(\n        'Do not replace the Vue.config object, set individual fields instead.'\n      );\n    };\n  }\n  Object.defineProperty(Vue, 'config', configDef);\n\n  // exposed util methods.\n  // NOTE: these are not considered part of the public API - avoid relying on\n  // them unless you are aware of the risk.\n  Vue.util = {\n    warn: warn,\n    extend: extend,\n    mergeOptions: mergeOptions,\n    defineReactive: defineReactive\n  };\n\n  Vue.set = set;\n  Vue.delete = del;\n  Vue.nextTick = nextTick;\n\n  Vue.options = Object.create(null);\n  ASSET_TYPES.forEach(function (type) {\n    Vue.options[type + 's'] = Object.create(null);\n  });\n\n  // this is used to identify the \"base\" constructor to extend all plain-object\n  // components with in Weex's multi-instance scenarios.\n  Vue.options._base = Vue;\n\n  extend(Vue.options.components, builtInComponents);\n\n  initUse(Vue);\n  initMixin$1(Vue);\n  initExtend(Vue);\n  initAssetRegisters(Vue);\n}\n\ninitGlobalAPI(Vue$3);\n\nObject.defineProperty(Vue$3.prototype, '$isServer', {\n  get: isServerRendering\n});\n\nObject.defineProperty(Vue$3.prototype, '$ssrContext', {\n  get: function get () {\n    /* istanbul ignore next */\n    return this.$vnode && this.$vnode.ssrContext\n  }\n});\n\nVue$3.version = '2.5.10';\n\n/*  */\n\n// these are reserved for web because they are directly compiled away\n// during template compilation\nvar isReservedAttr = makeMap('style,class');\n\n// attributes that should be using props for binding\nvar acceptValue = makeMap('input,textarea,option,select,progress');\nvar mustUseProp = function (tag, type, attr) {\n  return (\n    (attr === 'value' && acceptValue(tag)) && type !== 'button' ||\n    (attr === 'selected' && tag === 'option') ||\n    (attr === 'checked' && tag === 'input') ||\n    (attr === 'muted' && tag === 'video')\n  )\n};\n\nvar isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck');\n\nvar isBooleanAttr = makeMap(\n  'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' +\n  'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' +\n  'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' +\n  'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' +\n  'required,reversed,scoped,seamless,selected,sortable,translate,' +\n  'truespeed,typemustmatch,visible'\n);\n\nvar xlinkNS = 'http://www.w3.org/1999/xlink';\n\nvar isXlink = function (name) {\n  return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink'\n};\n\nvar getXlinkProp = function (name) {\n  return isXlink(name) ? name.slice(6, name.length) : ''\n};\n\nvar isFalsyAttrValue = function (val) {\n  return val == null || val === false\n};\n\n/*  */\n\nfunction genClassForVnode (vnode) {\n  var data = vnode.data;\n  var parentNode = vnode;\n  var childNode = vnode;\n  while (isDef(childNode.componentInstance)) {\n    childNode = childNode.componentInstance._vnode;\n    if (childNode.data) {\n      data = mergeClassData(childNode.data, data);\n    }\n  }\n  while (isDef(parentNode = parentNode.parent)) {\n    if (parentNode.data) {\n      data = mergeClassData(data, parentNode.data);\n    }\n  }\n  return renderClass(data.staticClass, data.class)\n}\n\nfunction mergeClassData (child, parent) {\n  return {\n    staticClass: concat(child.staticClass, parent.staticClass),\n    class: isDef(child.class)\n      ? [child.class, parent.class]\n      : parent.class\n  }\n}\n\nfunction renderClass (\n  staticClass,\n  dynamicClass\n) {\n  if (isDef(staticClass) || isDef(dynamicClass)) {\n    return concat(staticClass, stringifyClass(dynamicClass))\n  }\n  /* istanbul ignore next */\n  return ''\n}\n\nfunction concat (a, b) {\n  return a ? b ? (a + ' ' + b) : a : (b || '')\n}\n\nfunction stringifyClass (value) {\n  if (Array.isArray(value)) {\n    return stringifyArray(value)\n  }\n  if (isObject(value)) {\n    return stringifyObject(value)\n  }\n  if (typeof value === 'string') {\n    return value\n  }\n  /* istanbul ignore next */\n  return ''\n}\n\nfunction stringifyArray (value) {\n  var res = '';\n  var stringified;\n  for (var i = 0, l = value.length; i < l; i++) {\n    if (isDef(stringified = stringifyClass(value[i])) && stringified !== '') {\n      if (res) { res += ' '; }\n      res += stringified;\n    }\n  }\n  return res\n}\n\nfunction stringifyObject (value) {\n  var res = '';\n  for (var key in value) {\n    if (value[key]) {\n      if (res) { res += ' '; }\n      res += key;\n    }\n  }\n  return res\n}\n\n/*  */\n\nvar namespaceMap = {\n  svg: 'http://www.w3.org/2000/svg',\n  math: 'http://www.w3.org/1998/Math/MathML'\n};\n\nvar isHTMLTag = makeMap(\n  'html,body,base,head,link,meta,style,title,' +\n  'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +\n  'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +\n  'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +\n  's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +\n  'embed,object,param,source,canvas,script,noscript,del,ins,' +\n  'caption,col,colgroup,table,thead,tbody,td,th,tr,' +\n  'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +\n  'output,progress,select,textarea,' +\n  'details,dialog,menu,menuitem,summary,' +\n  'content,element,shadow,template,blockquote,iframe,tfoot'\n);\n\n// this map is intentionally selective, only covering SVG elements that may\n// contain child elements.\nvar isSVG = makeMap(\n  'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +\n  'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +\n  'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',\n  true\n);\n\nvar isPreTag = function (tag) { return tag === 'pre'; };\n\nvar isReservedTag = function (tag) {\n  return isHTMLTag(tag) || isSVG(tag)\n};\n\nfunction getTagNamespace (tag) {\n  if (isSVG(tag)) {\n    return 'svg'\n  }\n  // basic support for MathML\n  // note it doesn't support other MathML elements being component roots\n  if (tag === 'math') {\n    return 'math'\n  }\n}\n\nvar unknownElementCache = Object.create(null);\nfunction isUnknownElement (tag) {\n  /* istanbul ignore if */\n  if (!inBrowser) {\n    return true\n  }\n  if (isReservedTag(tag)) {\n    return false\n  }\n  tag = tag.toLowerCase();\n  /* istanbul ignore if */\n  if (unknownElementCache[tag] != null) {\n    return unknownElementCache[tag]\n  }\n  var el = document.createElement(tag);\n  if (tag.indexOf('-') > -1) {\n    // http://stackoverflow.com/a/28210364/1070244\n    return (unknownElementCache[tag] = (\n      el.constructor === window.HTMLUnknownElement ||\n      el.constructor === window.HTMLElement\n    ))\n  } else {\n    return (unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()))\n  }\n}\n\nvar isTextInputType = makeMap('text,number,password,search,email,tel,url');\n\n/*  */\n\n/**\n * Query an element selector if it's not an element already.\n */\nfunction query (el) {\n  if (typeof el === 'string') {\n    var selected = document.querySelector(el);\n    if (!selected) {\n      \"development\" !== 'production' && warn(\n        'Cannot find element: ' + el\n      );\n      return document.createElement('div')\n    }\n    return selected\n  } else {\n    return el\n  }\n}\n\n/*  */\n\nfunction createElement$1 (tagName, vnode) {\n  var elm = document.createElement(tagName);\n  if (tagName !== 'select') {\n    return elm\n  }\n  // false or null will remove the attribute but undefined will not\n  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {\n    elm.setAttribute('multiple', 'multiple');\n  }\n  return elm\n}\n\nfunction createElementNS (namespace, tagName) {\n  return document.createElementNS(namespaceMap[namespace], tagName)\n}\n\nfunction createTextNode (text) {\n  return document.createTextNode(text)\n}\n\nfunction createComment (text) {\n  return document.createComment(text)\n}\n\nfunction insertBefore (parentNode, newNode, referenceNode) {\n  parentNode.insertBefore(newNode, referenceNode);\n}\n\nfunction removeChild (node, child) {\n  node.removeChild(child);\n}\n\nfunction appendChild (node, child) {\n  node.appendChild(child);\n}\n\nfunction parentNode (node) {\n  return node.parentNode\n}\n\nfunction nextSibling (node) {\n  return node.nextSibling\n}\n\nfunction tagName (node) {\n  return node.tagName\n}\n\nfunction setTextContent (node, text) {\n  node.textContent = text;\n}\n\nfunction setAttribute (node, key, val) {\n  node.setAttribute(key, val);\n}\n\n\nvar nodeOps = Object.freeze({\n\tcreateElement: createElement$1,\n\tcreateElementNS: createElementNS,\n\tcreateTextNode: createTextNode,\n\tcreateComment: createComment,\n\tinsertBefore: insertBefore,\n\tremoveChild: removeChild,\n\tappendChild: appendChild,\n\tparentNode: parentNode,\n\tnextSibling: nextSibling,\n\ttagName: tagName,\n\tsetTextContent: setTextContent,\n\tsetAttribute: setAttribute\n});\n\n/*  */\n\nvar ref = {\n  create: function create (_, vnode) {\n    registerRef(vnode);\n  },\n  update: function update (oldVnode, vnode) {\n    if (oldVnode.data.ref !== vnode.data.ref) {\n      registerRef(oldVnode, true);\n      registerRef(vnode);\n    }\n  },\n  destroy: function destroy (vnode) {\n    registerRef(vnode, true);\n  }\n};\n\nfunction registerRef (vnode, isRemoval) {\n  var key = vnode.data.ref;\n  if (!key) { return }\n\n  var vm = vnode.context;\n  var ref = vnode.componentInstance || vnode.elm;\n  var refs = vm.$refs;\n  if (isRemoval) {\n    if (Array.isArray(refs[key])) {\n      remove(refs[key], ref);\n    } else if (refs[key] === ref) {\n      refs[key] = undefined;\n    }\n  } else {\n    if (vnode.data.refInFor) {\n      if (!Array.isArray(refs[key])) {\n        refs[key] = [ref];\n      } else if (refs[key].indexOf(ref) < 0) {\n        // $flow-disable-line\n        refs[key].push(ref);\n      }\n    } else {\n      refs[key] = ref;\n    }\n  }\n}\n\n/**\n * Virtual DOM patching algorithm based on Snabbdom by\n * Simon Friis Vindum (@paldepind)\n * Licensed under the MIT License\n * https://github.com/paldepind/snabbdom/blob/master/LICENSE\n *\n * modified by Evan You (@yyx990803)\n *\n * Not type-checking this because this file is perf-critical and the cost\n * of making flow understand it is not worth it.\n */\n\nvar emptyNode = new VNode('', {}, []);\n\nvar hooks = ['create', 'activate', 'update', 'remove', 'destroy'];\n\nfunction sameVnode (a, b) {\n  return (\n    a.key === b.key && (\n      (\n        a.tag === b.tag &&\n        a.isComment === b.isComment &&\n        isDef(a.data) === isDef(b.data) &&\n        sameInputType(a, b)\n      ) || (\n        isTrue(a.isAsyncPlaceholder) &&\n        a.asyncFactory === b.asyncFactory &&\n        isUndef(b.asyncFactory.error)\n      )\n    )\n  )\n}\n\nfunction sameInputType (a, b) {\n  if (a.tag !== 'input') { return true }\n  var i;\n  var typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type;\n  var typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type;\n  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)\n}\n\nfunction createKeyToOldIdx (children, beginIdx, endIdx) {\n  var i, key;\n  var map = {};\n  for (i = beginIdx; i <= endIdx; ++i) {\n    key = children[i].key;\n    if (isDef(key)) { map[key] = i; }\n  }\n  return map\n}\n\nfunction createPatchFunction (backend) {\n  var i, j;\n  var cbs = {};\n\n  var modules = backend.modules;\n  var nodeOps = backend.nodeOps;\n\n  for (i = 0; i < hooks.length; ++i) {\n    cbs[hooks[i]] = [];\n    for (j = 0; j < modules.length; ++j) {\n      if (isDef(modules[j][hooks[i]])) {\n        cbs[hooks[i]].push(modules[j][hooks[i]]);\n      }\n    }\n  }\n\n  function emptyNodeAt (elm) {\n    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)\n  }\n\n  function createRmCb (childElm, listeners) {\n    function remove () {\n      if (--remove.listeners === 0) {\n        removeNode(childElm);\n      }\n    }\n    remove.listeners = listeners;\n    return remove\n  }\n\n  function removeNode (el) {\n    var parent = nodeOps.parentNode(el);\n    // element may have already been removed due to v-html / v-text\n    if (isDef(parent)) {\n      nodeOps.removeChild(parent, el);\n    }\n  }\n\n  function isUnknownElement$$1 (vnode, inVPre) {\n    return (\n      !inVPre &&\n      !vnode.ns &&\n      !(\n        config.ignoredElements.length &&\n        config.ignoredElements.some(function (ignore) {\n          return isRegExp(ignore)\n            ? ignore.test(vnode.tag)\n            : ignore === vnode.tag\n        })\n      ) &&\n      config.isUnknownElement(vnode.tag)\n    )\n  }\n\n  var creatingElmInVPre = 0;\n  function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {\n    vnode.isRootInsert = !nested; // for transition enter check\n    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {\n      return\n    }\n\n    var data = vnode.data;\n    var children = vnode.children;\n    var tag = vnode.tag;\n    if (isDef(tag)) {\n      {\n        if (data && data.pre) {\n          creatingElmInVPre++;\n        }\n        if (isUnknownElement$$1(vnode, creatingElmInVPre)) {\n          warn(\n            'Unknown custom element: <' + tag + '> - did you ' +\n            'register the component correctly? For recursive components, ' +\n            'make sure to provide the \"name\" option.',\n            vnode.context\n          );\n        }\n      }\n      vnode.elm = vnode.ns\n        ? nodeOps.createElementNS(vnode.ns, tag)\n        : nodeOps.createElement(tag, vnode);\n      setScope(vnode);\n\n      /* istanbul ignore if */\n      {\n        createChildren(vnode, children, insertedVnodeQueue);\n        if (isDef(data)) {\n          invokeCreateHooks(vnode, insertedVnodeQueue);\n        }\n        insert(parentElm, vnode.elm, refElm);\n      }\n\n      if (\"development\" !== 'production' && data && data.pre) {\n        creatingElmInVPre--;\n      }\n    } else if (isTrue(vnode.isComment)) {\n      vnode.elm = nodeOps.createComment(vnode.text);\n      insert(parentElm, vnode.elm, refElm);\n    } else {\n      vnode.elm = nodeOps.createTextNode(vnode.text);\n      insert(parentElm, vnode.elm, refElm);\n    }\n  }\n\n  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {\n    var i = vnode.data;\n    if (isDef(i)) {\n      var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;\n      if (isDef(i = i.hook) && isDef(i = i.init)) {\n        i(vnode, false /* hydrating */, parentElm, refElm);\n      }\n      // after calling the init hook, if the vnode is a child component\n      // it should've created a child instance and mounted it. the child\n      // component also has set the placeholder vnode's elm.\n      // in that case we can just return the element and be done.\n      if (isDef(vnode.componentInstance)) {\n        initComponent(vnode, insertedVnodeQueue);\n        if (isTrue(isReactivated)) {\n          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);\n        }\n        return true\n      }\n    }\n  }\n\n  function initComponent (vnode, insertedVnodeQueue) {\n    if (isDef(vnode.data.pendingInsert)) {\n      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);\n      vnode.data.pendingInsert = null;\n    }\n    vnode.elm = vnode.componentInstance.$el;\n    if (isPatchable(vnode)) {\n      invokeCreateHooks(vnode, insertedVnodeQueue);\n      setScope(vnode);\n    } else {\n      // empty component root.\n      // skip all element-related modules except for ref (#3455)\n      registerRef(vnode);\n      // make sure to invoke the insert hook\n      insertedVnodeQueue.push(vnode);\n    }\n  }\n\n  function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {\n    var i;\n    // hack for #4339: a reactivated component with inner transition\n    // does not trigger because the inner node's created hooks are not called\n    // again. It's not ideal to involve module-specific logic in here but\n    // there doesn't seem to be a better way to do it.\n    var innerNode = vnode;\n    while (innerNode.componentInstance) {\n      innerNode = innerNode.componentInstance._vnode;\n      if (isDef(i = innerNode.data) && isDef(i = i.transition)) {\n        for (i = 0; i < cbs.activate.length; ++i) {\n          cbs.activate[i](emptyNode, innerNode);\n        }\n        insertedVnodeQueue.push(innerNode);\n        break\n      }\n    }\n    // unlike a newly created component,\n    // a reactivated keep-alive component doesn't insert itself\n    insert(parentElm, vnode.elm, refElm);\n  }\n\n  function insert (parent, elm, ref$$1) {\n    if (isDef(parent)) {\n      if (isDef(ref$$1)) {\n        if (ref$$1.parentNode === parent) {\n          nodeOps.insertBefore(parent, elm, ref$$1);\n        }\n      } else {\n        nodeOps.appendChild(parent, elm);\n      }\n    }\n  }\n\n  function createChildren (vnode, children, insertedVnodeQueue) {\n    if (Array.isArray(children)) {\n      {\n        checkDuplicateKeys(children);\n      }\n      for (var i = 0; i < children.length; ++i) {\n        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true);\n      }\n    } else if (isPrimitive(vnode.text)) {\n      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));\n    }\n  }\n\n  function isPatchable (vnode) {\n    while (vnode.componentInstance) {\n      vnode = vnode.componentInstance._vnode;\n    }\n    return isDef(vnode.tag)\n  }\n\n  function invokeCreateHooks (vnode, insertedVnodeQueue) {\n    for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {\n      cbs.create[i$1](emptyNode, vnode);\n    }\n    i = vnode.data.hook; // Reuse variable\n    if (isDef(i)) {\n      if (isDef(i.create)) { i.create(emptyNode, vnode); }\n      if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }\n    }\n  }\n\n  // set scope id attribute for scoped CSS.\n  // this is implemented as a special case to avoid the overhead\n  // of going through the normal attribute patching process.\n  function setScope (vnode) {\n    var i;\n    if (isDef(i = vnode.fnScopeId)) {\n      nodeOps.setAttribute(vnode.elm, i, '');\n    } else {\n      var ancestor = vnode;\n      while (ancestor) {\n        if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {\n          nodeOps.setAttribute(vnode.elm, i, '');\n        }\n        ancestor = ancestor.parent;\n      }\n    }\n    // for slot content they should also get the scopeId from the host instance.\n    if (isDef(i = activeInstance) &&\n      i !== vnode.context &&\n      i !== vnode.fnContext &&\n      isDef(i = i.$options._scopeId)\n    ) {\n      nodeOps.setAttribute(vnode.elm, i, '');\n    }\n  }\n\n  function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {\n    for (; startIdx <= endIdx; ++startIdx) {\n      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm);\n    }\n  }\n\n  function invokeDestroyHook (vnode) {\n    var i, j;\n    var data = vnode.data;\n    if (isDef(data)) {\n      if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }\n      for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }\n    }\n    if (isDef(i = vnode.children)) {\n      for (j = 0; j < vnode.children.length; ++j) {\n        invokeDestroyHook(vnode.children[j]);\n      }\n    }\n  }\n\n  function removeVnodes (parentElm, vnodes, startIdx, endIdx) {\n    for (; startIdx <= endIdx; ++startIdx) {\n      var ch = vnodes[startIdx];\n      if (isDef(ch)) {\n        if (isDef(ch.tag)) {\n          removeAndInvokeRemoveHook(ch);\n          invokeDestroyHook(ch);\n        } else { // Text node\n          removeNode(ch.elm);\n        }\n      }\n    }\n  }\n\n  function removeAndInvokeRemoveHook (vnode, rm) {\n    if (isDef(rm) || isDef(vnode.data)) {\n      var i;\n      var listeners = cbs.remove.length + 1;\n      if (isDef(rm)) {\n        // we have a recursively passed down rm callback\n        // increase the listeners count\n        rm.listeners += listeners;\n      } else {\n        // directly removing\n        rm = createRmCb(vnode.elm, listeners);\n      }\n      // recursively invoke hooks on child component root node\n      if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {\n        removeAndInvokeRemoveHook(i, rm);\n      }\n      for (i = 0; i < cbs.remove.length; ++i) {\n        cbs.remove[i](vnode, rm);\n      }\n      if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {\n        i(vnode, rm);\n      } else {\n        rm();\n      }\n    } else {\n      removeNode(vnode.elm);\n    }\n  }\n\n  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {\n    var oldStartIdx = 0;\n    var newStartIdx = 0;\n    var oldEndIdx = oldCh.length - 1;\n    var oldStartVnode = oldCh[0];\n    var oldEndVnode = oldCh[oldEndIdx];\n    var newEndIdx = newCh.length - 1;\n    var newStartVnode = newCh[0];\n    var newEndVnode = newCh[newEndIdx];\n    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;\n\n    // removeOnly is a special flag used only by <transition-group>\n    // to ensure removed elements stay in correct relative positions\n    // during leaving transitions\n    var canMove = !removeOnly;\n\n    {\n      checkDuplicateKeys(newCh);\n    }\n\n    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {\n      if (isUndef(oldStartVnode)) {\n        oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left\n      } else if (isUndef(oldEndVnode)) {\n        oldEndVnode = oldCh[--oldEndIdx];\n      } else if (sameVnode(oldStartVnode, newStartVnode)) {\n        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);\n        oldStartVnode = oldCh[++oldStartIdx];\n        newStartVnode = newCh[++newStartIdx];\n      } else if (sameVnode(oldEndVnode, newEndVnode)) {\n        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);\n        oldEndVnode = oldCh[--oldEndIdx];\n        newEndVnode = newCh[--newEndIdx];\n      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right\n        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);\n        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));\n        oldStartVnode = oldCh[++oldStartIdx];\n        newEndVnode = newCh[--newEndIdx];\n      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left\n        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);\n        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);\n        oldEndVnode = oldCh[--oldEndIdx];\n        newStartVnode = newCh[++newStartIdx];\n      } else {\n        if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }\n        idxInOld = isDef(newStartVnode.key)\n          ? oldKeyToIdx[newStartVnode.key]\n          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);\n        if (isUndef(idxInOld)) { // New element\n          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm);\n        } else {\n          vnodeToMove = oldCh[idxInOld];\n          if (sameVnode(vnodeToMove, newStartVnode)) {\n            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);\n            oldCh[idxInOld] = undefined;\n            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);\n          } else {\n            // same key but different element. treat as new element\n            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm);\n          }\n        }\n        newStartVnode = newCh[++newStartIdx];\n      }\n    }\n    if (oldStartIdx > oldEndIdx) {\n      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;\n      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);\n    } else if (newStartIdx > newEndIdx) {\n      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);\n    }\n  }\n\n  function checkDuplicateKeys (children) {\n    var seenKeys = {};\n    for (var i = 0; i < children.length; i++) {\n      var vnode = children[i];\n      var key = vnode.key;\n      if (isDef(key)) {\n        if (seenKeys[key]) {\n          warn(\n            (\"Duplicate keys detected: '\" + key + \"'. This may cause an update error.\"),\n            vnode.context\n          );\n        } else {\n          seenKeys[key] = true;\n        }\n      }\n    }\n  }\n\n  function findIdxInOld (node, oldCh, start, end) {\n    for (var i = start; i < end; i++) {\n      var c = oldCh[i];\n      if (isDef(c) && sameVnode(node, c)) { return i }\n    }\n  }\n\n  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {\n    if (oldVnode === vnode) {\n      return\n    }\n\n    var elm = vnode.elm = oldVnode.elm;\n\n    if (isTrue(oldVnode.isAsyncPlaceholder)) {\n      if (isDef(vnode.asyncFactory.resolved)) {\n        hydrate(oldVnode.elm, vnode, insertedVnodeQueue);\n      } else {\n        vnode.isAsyncPlaceholder = true;\n      }\n      return\n    }\n\n    // reuse element for static trees.\n    // note we only do this if the vnode is cloned -\n    // if the new node is not cloned it means the render functions have been\n    // reset by the hot-reload-api and we need to do a proper re-render.\n    if (isTrue(vnode.isStatic) &&\n      isTrue(oldVnode.isStatic) &&\n      vnode.key === oldVnode.key &&\n      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))\n    ) {\n      vnode.componentInstance = oldVnode.componentInstance;\n      return\n    }\n\n    var i;\n    var data = vnode.data;\n    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {\n      i(oldVnode, vnode);\n    }\n\n    var oldCh = oldVnode.children;\n    var ch = vnode.children;\n    if (isDef(data) && isPatchable(vnode)) {\n      for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }\n      if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }\n    }\n    if (isUndef(vnode.text)) {\n      if (isDef(oldCh) && isDef(ch)) {\n        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }\n      } else if (isDef(ch)) {\n        if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }\n        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);\n      } else if (isDef(oldCh)) {\n        removeVnodes(elm, oldCh, 0, oldCh.length - 1);\n      } else if (isDef(oldVnode.text)) {\n        nodeOps.setTextContent(elm, '');\n      }\n    } else if (oldVnode.text !== vnode.text) {\n      nodeOps.setTextContent(elm, vnode.text);\n    }\n    if (isDef(data)) {\n      if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }\n    }\n  }\n\n  function invokeInsertHook (vnode, queue, initial) {\n    // delay insert hooks for component root nodes, invoke them after the\n    // element is really inserted\n    if (isTrue(initial) && isDef(vnode.parent)) {\n      vnode.parent.data.pendingInsert = queue;\n    } else {\n      for (var i = 0; i < queue.length; ++i) {\n        queue[i].data.hook.insert(queue[i]);\n      }\n    }\n  }\n\n  var hydrationBailed = false;\n  // list of modules that can skip create hook during hydration because they\n  // are already rendered on the client or has no need for initialization\n  // Note: style is excluded because it relies on initial clone for future\n  // deep updates (#7063).\n  var isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key');\n\n  // Note: this is a browser-only function so we can assume elms are DOM nodes.\n  function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {\n    var i;\n    var tag = vnode.tag;\n    var data = vnode.data;\n    var children = vnode.children;\n    inVPre = inVPre || (data && data.pre);\n    vnode.elm = elm;\n\n    if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {\n      vnode.isAsyncPlaceholder = true;\n      return true\n    }\n    // assert node match\n    {\n      if (!assertNodeMatch(elm, vnode, inVPre)) {\n        return false\n      }\n    }\n    if (isDef(data)) {\n      if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */); }\n      if (isDef(i = vnode.componentInstance)) {\n        // child component. it should have hydrated its own tree.\n        initComponent(vnode, insertedVnodeQueue);\n        return true\n      }\n    }\n    if (isDef(tag)) {\n      if (isDef(children)) {\n        // empty element, allow client to pick up and populate children\n        if (!elm.hasChildNodes()) {\n          createChildren(vnode, children, insertedVnodeQueue);\n        } else {\n          // v-html and domProps: innerHTML\n          if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {\n            if (i !== elm.innerHTML) {\n              /* istanbul ignore if */\n              if (\"development\" !== 'production' &&\n                typeof console !== 'undefined' &&\n                !hydrationBailed\n              ) {\n                hydrationBailed = true;\n                console.warn('Parent: ', elm);\n                console.warn('server innerHTML: ', i);\n                console.warn('client innerHTML: ', elm.innerHTML);\n              }\n              return false\n            }\n          } else {\n            // iterate and compare children lists\n            var childrenMatch = true;\n            var childNode = elm.firstChild;\n            for (var i$1 = 0; i$1 < children.length; i$1++) {\n              if (!childNode || !hydrate(childNode, children[i$1], insertedVnodeQueue, inVPre)) {\n                childrenMatch = false;\n                break\n              }\n              childNode = childNode.nextSibling;\n            }\n            // if childNode is not null, it means the actual childNodes list is\n            // longer than the virtual children list.\n            if (!childrenMatch || childNode) {\n              /* istanbul ignore if */\n              if (\"development\" !== 'production' &&\n                typeof console !== 'undefined' &&\n                !hydrationBailed\n              ) {\n                hydrationBailed = true;\n                console.warn('Parent: ', elm);\n                console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children);\n              }\n              return false\n            }\n          }\n        }\n      }\n      if (isDef(data)) {\n        var fullInvoke = false;\n        for (var key in data) {\n          if (!isRenderedModule(key)) {\n            fullInvoke = true;\n            invokeCreateHooks(vnode, insertedVnodeQueue);\n            break\n          }\n        }\n        if (!fullInvoke && data['class']) {\n          // ensure collecting deps for deep class bindings for future updates\n          traverse(data['class']);\n        }\n      }\n    } else if (elm.data !== vnode.text) {\n      elm.data = vnode.text;\n    }\n    return true\n  }\n\n  function assertNodeMatch (node, vnode, inVPre) {\n    if (isDef(vnode.tag)) {\n      return vnode.tag.indexOf('vue-component') === 0 || (\n        !isUnknownElement$$1(vnode, inVPre) &&\n        vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase())\n      )\n    } else {\n      return node.nodeType === (vnode.isComment ? 8 : 3)\n    }\n  }\n\n  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {\n    if (isUndef(vnode)) {\n      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }\n      return\n    }\n\n    var isInitialPatch = false;\n    var insertedVnodeQueue = [];\n\n    if (isUndef(oldVnode)) {\n      // empty mount (likely as component), create new root element\n      isInitialPatch = true;\n      createElm(vnode, insertedVnodeQueue, parentElm, refElm);\n    } else {\n      var isRealElement = isDef(oldVnode.nodeType);\n      if (!isRealElement && sameVnode(oldVnode, vnode)) {\n        // patch existing root node\n        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);\n      } else {\n        if (isRealElement) {\n          // mounting to a real element\n          // check if this is server-rendered content and if we can perform\n          // a successful hydration.\n          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {\n            oldVnode.removeAttribute(SSR_ATTR);\n            hydrating = true;\n          }\n          if (isTrue(hydrating)) {\n            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {\n              invokeInsertHook(vnode, insertedVnodeQueue, true);\n              return oldVnode\n            } else {\n              warn(\n                'The client-side rendered virtual DOM tree is not matching ' +\n                'server-rendered content. This is likely caused by incorrect ' +\n                'HTML markup, for example nesting block-level elements inside ' +\n                '<p>, or missing <tbody>. Bailing hydration and performing ' +\n                'full client-side render.'\n              );\n            }\n          }\n          // either not server-rendered, or hydration failed.\n          // create an empty node and replace it\n          oldVnode = emptyNodeAt(oldVnode);\n        }\n\n        // replacing existing element\n        var oldElm = oldVnode.elm;\n        var parentElm$1 = nodeOps.parentNode(oldElm);\n\n        // create new node\n        createElm(\n          vnode,\n          insertedVnodeQueue,\n          // extremely rare edge case: do not insert if old element is in a\n          // leaving transition. Only happens when combining transition +\n          // keep-alive + HOCs. (#4590)\n          oldElm._leaveCb ? null : parentElm$1,\n          nodeOps.nextSibling(oldElm)\n        );\n\n        // update parent placeholder node element, recursively\n        if (isDef(vnode.parent)) {\n          var ancestor = vnode.parent;\n          var patchable = isPatchable(vnode);\n          while (ancestor) {\n            for (var i = 0; i < cbs.destroy.length; ++i) {\n              cbs.destroy[i](ancestor);\n            }\n            ancestor.elm = vnode.elm;\n            if (patchable) {\n              for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {\n                cbs.create[i$1](emptyNode, ancestor);\n              }\n              // #6513\n              // invoke insert hooks that may have been merged by create hooks.\n              // e.g. for directives that uses the \"inserted\" hook.\n              var insert = ancestor.data.hook.insert;\n              if (insert.merged) {\n                // start at index 1 to avoid re-invoking component mounted hook\n                for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {\n                  insert.fns[i$2]();\n                }\n              }\n            } else {\n              registerRef(ancestor);\n            }\n            ancestor = ancestor.parent;\n          }\n        }\n\n        // destroy old node\n        if (isDef(parentElm$1)) {\n          removeVnodes(parentElm$1, [oldVnode], 0, 0);\n        } else if (isDef(oldVnode.tag)) {\n          invokeDestroyHook(oldVnode);\n        }\n      }\n    }\n\n    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);\n    return vnode.elm\n  }\n}\n\n/*  */\n\nvar directives = {\n  create: updateDirectives,\n  update: updateDirectives,\n  destroy: function unbindDirectives (vnode) {\n    updateDirectives(vnode, emptyNode);\n  }\n};\n\nfunction updateDirectives (oldVnode, vnode) {\n  if (oldVnode.data.directives || vnode.data.directives) {\n    _update(oldVnode, vnode);\n  }\n}\n\nfunction _update (oldVnode, vnode) {\n  var isCreate = oldVnode === emptyNode;\n  var isDestroy = vnode === emptyNode;\n  var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);\n  var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);\n\n  var dirsWithInsert = [];\n  var dirsWithPostpatch = [];\n\n  var key, oldDir, dir;\n  for (key in newDirs) {\n    oldDir = oldDirs[key];\n    dir = newDirs[key];\n    if (!oldDir) {\n      // new directive, bind\n      callHook$1(dir, 'bind', vnode, oldVnode);\n      if (dir.def && dir.def.inserted) {\n        dirsWithInsert.push(dir);\n      }\n    } else {\n      // existing directive, update\n      dir.oldValue = oldDir.value;\n      callHook$1(dir, 'update', vnode, oldVnode);\n      if (dir.def && dir.def.componentUpdated) {\n        dirsWithPostpatch.push(dir);\n      }\n    }\n  }\n\n  if (dirsWithInsert.length) {\n    var callInsert = function () {\n      for (var i = 0; i < dirsWithInsert.length; i++) {\n        callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);\n      }\n    };\n    if (isCreate) {\n      mergeVNodeHook(vnode, 'insert', callInsert);\n    } else {\n      callInsert();\n    }\n  }\n\n  if (dirsWithPostpatch.length) {\n    mergeVNodeHook(vnode, 'postpatch', function () {\n      for (var i = 0; i < dirsWithPostpatch.length; i++) {\n        callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);\n      }\n    });\n  }\n\n  if (!isCreate) {\n    for (key in oldDirs) {\n      if (!newDirs[key]) {\n        // no longer present, unbind\n        callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);\n      }\n    }\n  }\n}\n\nvar emptyModifiers = Object.create(null);\n\nfunction normalizeDirectives$1 (\n  dirs,\n  vm\n) {\n  var res = Object.create(null);\n  if (!dirs) {\n    return res\n  }\n  var i, dir;\n  for (i = 0; i < dirs.length; i++) {\n    dir = dirs[i];\n    if (!dir.modifiers) {\n      dir.modifiers = emptyModifiers;\n    }\n    res[getRawDirName(dir)] = dir;\n    dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);\n  }\n  return res\n}\n\nfunction getRawDirName (dir) {\n  return dir.rawName || ((dir.name) + \".\" + (Object.keys(dir.modifiers || {}).join('.')))\n}\n\nfunction callHook$1 (dir, hook, vnode, oldVnode, isDestroy) {\n  var fn = dir.def && dir.def[hook];\n  if (fn) {\n    try {\n      fn(vnode.elm, dir, vnode, oldVnode, isDestroy);\n    } catch (e) {\n      handleError(e, vnode.context, (\"directive \" + (dir.name) + \" \" + hook + \" hook\"));\n    }\n  }\n}\n\nvar baseModules = [\n  ref,\n  directives\n];\n\n/*  */\n\nfunction updateAttrs (oldVnode, vnode) {\n  var opts = vnode.componentOptions;\n  if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {\n    return\n  }\n  if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {\n    return\n  }\n  var key, cur, old;\n  var elm = vnode.elm;\n  var oldAttrs = oldVnode.data.attrs || {};\n  var attrs = vnode.data.attrs || {};\n  // clone observed objects, as the user probably wants to mutate it\n  if (isDef(attrs.__ob__)) {\n    attrs = vnode.data.attrs = extend({}, attrs);\n  }\n\n  for (key in attrs) {\n    cur = attrs[key];\n    old = oldAttrs[key];\n    if (old !== cur) {\n      setAttr(elm, key, cur);\n    }\n  }\n  // #4391: in IE9, setting type can reset value for input[type=radio]\n  // #6666: IE/Edge forces progress value down to 1 before setting a max\n  /* istanbul ignore if */\n  if ((isIE || isEdge) && attrs.value !== oldAttrs.value) {\n    setAttr(elm, 'value', attrs.value);\n  }\n  for (key in oldAttrs) {\n    if (isUndef(attrs[key])) {\n      if (isXlink(key)) {\n        elm.removeAttributeNS(xlinkNS, getXlinkProp(key));\n      } else if (!isEnumeratedAttr(key)) {\n        elm.removeAttribute(key);\n      }\n    }\n  }\n}\n\nfunction setAttr (el, key, value) {\n  if (isBooleanAttr(key)) {\n    // set attribute for blank value\n    // e.g. <option disabled>Select one</option>\n    if (isFalsyAttrValue(value)) {\n      el.removeAttribute(key);\n    } else {\n      // technically allowfullscreen is a boolean attribute for <iframe>,\n      // but Flash expects a value of \"true\" when used on <embed> tag\n      value = key === 'allowfullscreen' && el.tagName === 'EMBED'\n        ? 'true'\n        : key;\n      el.setAttribute(key, value);\n    }\n  } else if (isEnumeratedAttr(key)) {\n    el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true');\n  } else if (isXlink(key)) {\n    if (isFalsyAttrValue(value)) {\n      el.removeAttributeNS(xlinkNS, getXlinkProp(key));\n    } else {\n      el.setAttributeNS(xlinkNS, key, value);\n    }\n  } else {\n    if (isFalsyAttrValue(value)) {\n      el.removeAttribute(key);\n    } else {\n      // #7138: IE10 & 11 fires input event when setting placeholder on\n      // <textarea>... block the first input event and remove the blocker\n      // immediately.\n      /* istanbul ignore if */\n      if (\n        isIE && !isIE9 &&\n        el.tagName === 'TEXTAREA' &&\n        key === 'placeholder' && !el.__ieph\n      ) {\n        var blocker = function (e) {\n          e.stopImmediatePropagation();\n          el.removeEventListener('input', blocker);\n        };\n        el.addEventListener('input', blocker);\n        // $flow-disable-line\n        el.__ieph = true; /* IE placeholder patched */\n      }\n      el.setAttribute(key, value);\n    }\n  }\n}\n\nvar attrs = {\n  create: updateAttrs,\n  update: updateAttrs\n};\n\n/*  */\n\nfunction updateClass (oldVnode, vnode) {\n  var el = vnode.elm;\n  var data = vnode.data;\n  var oldData = oldVnode.data;\n  if (\n    isUndef(data.staticClass) &&\n    isUndef(data.class) && (\n      isUndef(oldData) || (\n        isUndef(oldData.staticClass) &&\n        isUndef(oldData.class)\n      )\n    )\n  ) {\n    return\n  }\n\n  var cls = genClassForVnode(vnode);\n\n  // handle transition classes\n  var transitionClass = el._transitionClasses;\n  if (isDef(transitionClass)) {\n    cls = concat(cls, stringifyClass(transitionClass));\n  }\n\n  // set the class\n  if (cls !== el._prevClass) {\n    el.setAttribute('class', cls);\n    el._prevClass = cls;\n  }\n}\n\nvar klass = {\n  create: updateClass,\n  update: updateClass\n};\n\n/*  */\n\nvar validDivisionCharRE = /[\\w).+\\-_$\\]]/;\n\nfunction parseFilters (exp) {\n  var inSingle = false;\n  var inDouble = false;\n  var inTemplateString = false;\n  var inRegex = false;\n  var curly = 0;\n  var square = 0;\n  var paren = 0;\n  var lastFilterIndex = 0;\n  var c, prev, i, expression, filters;\n\n  for (i = 0; i < exp.length; i++) {\n    prev = c;\n    c = exp.charCodeAt(i);\n    if (inSingle) {\n      if (c === 0x27 && prev !== 0x5C) { inSingle = false; }\n    } else if (inDouble) {\n      if (c === 0x22 && prev !== 0x5C) { inDouble = false; }\n    } else if (inTemplateString) {\n      if (c === 0x60 && prev !== 0x5C) { inTemplateString = false; }\n    } else if (inRegex) {\n      if (c === 0x2f && prev !== 0x5C) { inRegex = false; }\n    } else if (\n      c === 0x7C && // pipe\n      exp.charCodeAt(i + 1) !== 0x7C &&\n      exp.charCodeAt(i - 1) !== 0x7C &&\n      !curly && !square && !paren\n    ) {\n      if (expression === undefined) {\n        // first filter, end of expression\n        lastFilterIndex = i + 1;\n        expression = exp.slice(0, i).trim();\n      } else {\n        pushFilter();\n      }\n    } else {\n      switch (c) {\n        case 0x22: inDouble = true; break         // \"\n        case 0x27: inSingle = true; break         // '\n        case 0x60: inTemplateString = true; break // `\n        case 0x28: paren++; break                 // (\n        case 0x29: paren--; break                 // )\n        case 0x5B: square++; break                // [\n        case 0x5D: square--; break                // ]\n        case 0x7B: curly++; break                 // {\n        case 0x7D: curly--; break                 // }\n      }\n      if (c === 0x2f) { // /\n        var j = i - 1;\n        var p = (void 0);\n        // find first non-whitespace prev char\n        for (; j >= 0; j--) {\n          p = exp.charAt(j);\n          if (p !== ' ') { break }\n        }\n        if (!p || !validDivisionCharRE.test(p)) {\n          inRegex = true;\n        }\n      }\n    }\n  }\n\n  if (expression === undefined) {\n    expression = exp.slice(0, i).trim();\n  } else if (lastFilterIndex !== 0) {\n    pushFilter();\n  }\n\n  function pushFilter () {\n    (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());\n    lastFilterIndex = i + 1;\n  }\n\n  if (filters) {\n    for (i = 0; i < filters.length; i++) {\n      expression = wrapFilter(expression, filters[i]);\n    }\n  }\n\n  return expression\n}\n\nfunction wrapFilter (exp, filter) {\n  var i = filter.indexOf('(');\n  if (i < 0) {\n    // _f: resolveFilter\n    return (\"_f(\\\"\" + filter + \"\\\")(\" + exp + \")\")\n  } else {\n    var name = filter.slice(0, i);\n    var args = filter.slice(i + 1);\n    return (\"_f(\\\"\" + name + \"\\\")(\" + exp + \",\" + args)\n  }\n}\n\n/*  */\n\nfunction baseWarn (msg) {\n  console.error((\"[Vue compiler]: \" + msg));\n}\n\nfunction pluckModuleFunction (\n  modules,\n  key\n) {\n  return modules\n    ? modules.map(function (m) { return m[key]; }).filter(function (_) { return _; })\n    : []\n}\n\nfunction addProp (el, name, value) {\n  (el.props || (el.props = [])).push({ name: name, value: value });\n}\n\nfunction addAttr (el, name, value) {\n  (el.attrs || (el.attrs = [])).push({ name: name, value: value });\n}\n\nfunction addDirective (\n  el,\n  name,\n  rawName,\n  value,\n  arg,\n  modifiers\n) {\n  (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });\n}\n\nfunction addHandler (\n  el,\n  name,\n  value,\n  modifiers,\n  important,\n  warn\n) {\n  modifiers = modifiers || emptyObject;\n  // warn prevent and passive modifier\n  /* istanbul ignore if */\n  if (\n    \"development\" !== 'production' && warn &&\n    modifiers.prevent && modifiers.passive\n  ) {\n    warn(\n      'passive and prevent can\\'t be used together. ' +\n      'Passive handler can\\'t prevent default event.'\n    );\n  }\n\n  // check capture modifier\n  if (modifiers.capture) {\n    delete modifiers.capture;\n    name = '!' + name; // mark the event as captured\n  }\n  if (modifiers.once) {\n    delete modifiers.once;\n    name = '~' + name; // mark the event as once\n  }\n  /* istanbul ignore if */\n  if (modifiers.passive) {\n    delete modifiers.passive;\n    name = '&' + name; // mark the event as passive\n  }\n\n  // normalize click.right and click.middle since they don't actually fire\n  // this is technically browser-specific, but at least for now browsers are\n  // the only target envs that have right/middle clicks.\n  if (name === 'click') {\n    if (modifiers.right) {\n      name = 'contextmenu';\n      delete modifiers.right;\n    } else if (modifiers.middle) {\n      name = 'mouseup';\n    }\n  }\n\n  var events;\n  if (modifiers.native) {\n    delete modifiers.native;\n    events = el.nativeEvents || (el.nativeEvents = {});\n  } else {\n    events = el.events || (el.events = {});\n  }\n\n  var newHandler = { value: value };\n  if (modifiers !== emptyObject) {\n    newHandler.modifiers = modifiers;\n  }\n\n  var handlers = events[name];\n  /* istanbul ignore if */\n  if (Array.isArray(handlers)) {\n    important ? handlers.unshift(newHandler) : handlers.push(newHandler);\n  } else if (handlers) {\n    events[name] = important ? [newHandler, handlers] : [handlers, newHandler];\n  } else {\n    events[name] = newHandler;\n  }\n}\n\nfunction getBindingAttr (\n  el,\n  name,\n  getStatic\n) {\n  var dynamicValue =\n    getAndRemoveAttr(el, ':' + name) ||\n    getAndRemoveAttr(el, 'v-bind:' + name);\n  if (dynamicValue != null) {\n    return parseFilters(dynamicValue)\n  } else if (getStatic !== false) {\n    var staticValue = getAndRemoveAttr(el, name);\n    if (staticValue != null) {\n      return JSON.stringify(staticValue)\n    }\n  }\n}\n\n// note: this only removes the attr from the Array (attrsList) so that it\n// doesn't get processed by processAttrs.\n// By default it does NOT remove it from the map (attrsMap) because the map is\n// needed during codegen.\nfunction getAndRemoveAttr (\n  el,\n  name,\n  removeFromMap\n) {\n  var val;\n  if ((val = el.attrsMap[name]) != null) {\n    var list = el.attrsList;\n    for (var i = 0, l = list.length; i < l; i++) {\n      if (list[i].name === name) {\n        list.splice(i, 1);\n        break\n      }\n    }\n  }\n  if (removeFromMap) {\n    delete el.attrsMap[name];\n  }\n  return val\n}\n\n/*  */\n\n/**\n * Cross-platform code generation for component v-model\n */\nfunction genComponentModel (\n  el,\n  value,\n  modifiers\n) {\n  var ref = modifiers || {};\n  var number = ref.number;\n  var trim = ref.trim;\n\n  var baseValueExpression = '$$v';\n  var valueExpression = baseValueExpression;\n  if (trim) {\n    valueExpression =\n      \"(typeof \" + baseValueExpression + \" === 'string'\" +\n        \"? \" + baseValueExpression + \".trim()\" +\n        \": \" + baseValueExpression + \")\";\n  }\n  if (number) {\n    valueExpression = \"_n(\" + valueExpression + \")\";\n  }\n  var assignment = genAssignmentCode(value, valueExpression);\n\n  el.model = {\n    value: (\"(\" + value + \")\"),\n    expression: (\"\\\"\" + value + \"\\\"\"),\n    callback: (\"function (\" + baseValueExpression + \") {\" + assignment + \"}\")\n  };\n}\n\n/**\n * Cross-platform codegen helper for generating v-model value assignment code.\n */\nfunction genAssignmentCode (\n  value,\n  assignment\n) {\n  var res = parseModel(value);\n  if (res.key === null) {\n    return (value + \"=\" + assignment)\n  } else {\n    return (\"$set(\" + (res.exp) + \", \" + (res.key) + \", \" + assignment + \")\")\n  }\n}\n\n/**\n * Parse a v-model expression into a base path and a final key segment.\n * Handles both dot-path and possible square brackets.\n *\n * Possible cases:\n *\n * - test\n * - test[key]\n * - test[test1[key]]\n * - test[\"a\"][key]\n * - xxx.test[a[a].test1[key]]\n * - test.xxx.a[\"asa\"][test1[key]]\n *\n */\n\nvar len;\nvar str;\nvar chr;\nvar index$1;\nvar expressionPos;\nvar expressionEndPos;\n\n\n\nfunction parseModel (val) {\n  len = val.length;\n\n  if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) {\n    index$1 = val.lastIndexOf('.');\n    if (index$1 > -1) {\n      return {\n        exp: val.slice(0, index$1),\n        key: '\"' + val.slice(index$1 + 1) + '\"'\n      }\n    } else {\n      return {\n        exp: val,\n        key: null\n      }\n    }\n  }\n\n  str = val;\n  index$1 = expressionPos = expressionEndPos = 0;\n\n  while (!eof()) {\n    chr = next();\n    /* istanbul ignore if */\n    if (isStringStart(chr)) {\n      parseString(chr);\n    } else if (chr === 0x5B) {\n      parseBracket(chr);\n    }\n  }\n\n  return {\n    exp: val.slice(0, expressionPos),\n    key: val.slice(expressionPos + 1, expressionEndPos)\n  }\n}\n\nfunction next () {\n  return str.charCodeAt(++index$1)\n}\n\nfunction eof () {\n  return index$1 >= len\n}\n\nfunction isStringStart (chr) {\n  return chr === 0x22 || chr === 0x27\n}\n\nfunction parseBracket (chr) {\n  var inBracket = 1;\n  expressionPos = index$1;\n  while (!eof()) {\n    chr = next();\n    if (isStringStart(chr)) {\n      parseString(chr);\n      continue\n    }\n    if (chr === 0x5B) { inBracket++; }\n    if (chr === 0x5D) { inBracket--; }\n    if (inBracket === 0) {\n      expressionEndPos = index$1;\n      break\n    }\n  }\n}\n\nfunction parseString (chr) {\n  var stringQuote = chr;\n  while (!eof()) {\n    chr = next();\n    if (chr === stringQuote) {\n      break\n    }\n  }\n}\n\n/*  */\n\nvar warn$1;\n\n// in some cases, the event used has to be determined at runtime\n// so we used some reserved tokens during compile.\nvar RANGE_TOKEN = '__r';\nvar CHECKBOX_RADIO_TOKEN = '__c';\n\nfunction model (\n  el,\n  dir,\n  _warn\n) {\n  warn$1 = _warn;\n  var value = dir.value;\n  var modifiers = dir.modifiers;\n  var tag = el.tag;\n  var type = el.attrsMap.type;\n\n  {\n    // inputs with type=\"file\" are read only and setting the input's\n    // value will throw an error.\n    if (tag === 'input' && type === 'file') {\n      warn$1(\n        \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\" type=\\\"file\\\">:\\n\" +\n        \"File inputs are read only. Use a v-on:change listener instead.\"\n      );\n    }\n  }\n\n  if (el.component) {\n    genComponentModel(el, value, modifiers);\n    // component v-model doesn't need extra runtime\n    return false\n  } else if (tag === 'select') {\n    genSelect(el, value, modifiers);\n  } else if (tag === 'input' && type === 'checkbox') {\n    genCheckboxModel(el, value, modifiers);\n  } else if (tag === 'input' && type === 'radio') {\n    genRadioModel(el, value, modifiers);\n  } else if (tag === 'input' || tag === 'textarea') {\n    genDefaultModel(el, value, modifiers);\n  } else if (!config.isReservedTag(tag)) {\n    genComponentModel(el, value, modifiers);\n    // component v-model doesn't need extra runtime\n    return false\n  } else {\n    warn$1(\n      \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\">: \" +\n      \"v-model is not supported on this element type. \" +\n      'If you are working with contenteditable, it\\'s recommended to ' +\n      'wrap a library dedicated for that purpose inside a custom component.'\n    );\n  }\n\n  // ensure runtime directive metadata\n  return true\n}\n\nfunction genCheckboxModel (\n  el,\n  value,\n  modifiers\n) {\n  var number = modifiers && modifiers.number;\n  var valueBinding = getBindingAttr(el, 'value') || 'null';\n  var trueValueBinding = getBindingAttr(el, 'true-value') || 'true';\n  var falseValueBinding = getBindingAttr(el, 'false-value') || 'false';\n  addProp(el, 'checked',\n    \"Array.isArray(\" + value + \")\" +\n      \"?_i(\" + value + \",\" + valueBinding + \")>-1\" + (\n        trueValueBinding === 'true'\n          ? (\":(\" + value + \")\")\n          : (\":_q(\" + value + \",\" + trueValueBinding + \")\")\n      )\n  );\n  addHandler(el, 'change',\n    \"var $$a=\" + value + \",\" +\n        '$$el=$event.target,' +\n        \"$$c=$$el.checked?(\" + trueValueBinding + \"):(\" + falseValueBinding + \");\" +\n    'if(Array.isArray($$a)){' +\n      \"var $$v=\" + (number ? '_n(' + valueBinding + ')' : valueBinding) + \",\" +\n          '$$i=_i($$a,$$v);' +\n      \"if($$el.checked){$$i<0&&(\" + value + \"=$$a.concat([$$v]))}\" +\n      \"else{$$i>-1&&(\" + value + \"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}\" +\n    \"}else{\" + (genAssignmentCode(value, '$$c')) + \"}\",\n    null, true\n  );\n}\n\nfunction genRadioModel (\n    el,\n    value,\n    modifiers\n) {\n  var number = modifiers && modifiers.number;\n  var valueBinding = getBindingAttr(el, 'value') || 'null';\n  valueBinding = number ? (\"_n(\" + valueBinding + \")\") : valueBinding;\n  addProp(el, 'checked', (\"_q(\" + value + \",\" + valueBinding + \")\"));\n  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true);\n}\n\nfunction genSelect (\n    el,\n    value,\n    modifiers\n) {\n  var number = modifiers && modifiers.number;\n  var selectedVal = \"Array.prototype.filter\" +\n    \".call($event.target.options,function(o){return o.selected})\" +\n    \".map(function(o){var val = \\\"_value\\\" in o ? o._value : o.value;\" +\n    \"return \" + (number ? '_n(val)' : 'val') + \"})\";\n\n  var assignment = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]';\n  var code = \"var $$selectedVal = \" + selectedVal + \";\";\n  code = code + \" \" + (genAssignmentCode(value, assignment));\n  addHandler(el, 'change', code, null, true);\n}\n\nfunction genDefaultModel (\n  el,\n  value,\n  modifiers\n) {\n  var type = el.attrsMap.type;\n\n  // warn if v-bind:value conflicts with v-model\n  {\n    var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];\n    if (value$1) {\n      var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';\n      warn$1(\n        binding + \"=\\\"\" + value$1 + \"\\\" conflicts with v-model on the same element \" +\n        'because the latter already expands to a value binding internally'\n      );\n    }\n  }\n\n  var ref = modifiers || {};\n  var lazy = ref.lazy;\n  var number = ref.number;\n  var trim = ref.trim;\n  var needCompositionGuard = !lazy && type !== 'range';\n  var event = lazy\n    ? 'change'\n    : type === 'range'\n      ? RANGE_TOKEN\n      : 'input';\n\n  var valueExpression = '$event.target.value';\n  if (trim) {\n    valueExpression = \"$event.target.value.trim()\";\n  }\n  if (number) {\n    valueExpression = \"_n(\" + valueExpression + \")\";\n  }\n\n  var code = genAssignmentCode(value, valueExpression);\n  if (needCompositionGuard) {\n    code = \"if($event.target.composing)return;\" + code;\n  }\n\n  addProp(el, 'value', (\"(\" + value + \")\"));\n  addHandler(el, event, code, null, true);\n  if (trim || number) {\n    addHandler(el, 'blur', '$forceUpdate()');\n  }\n}\n\n/*  */\n\n// normalize v-model event tokens that can only be determined at runtime.\n// it's important to place the event as the first in the array because\n// the whole point is ensuring the v-model callback gets called before\n// user-attached handlers.\nfunction normalizeEvents (on) {\n  /* istanbul ignore if */\n  if (isDef(on[RANGE_TOKEN])) {\n    // IE input[type=range] only supports `change` event\n    var event = isIE ? 'change' : 'input';\n    on[event] = [].concat(on[RANGE_TOKEN], on[event] || []);\n    delete on[RANGE_TOKEN];\n  }\n  // This was originally intended to fix #4521 but no longer necessary\n  // after 2.5. Keeping it for backwards compat with generated code from < 2.4\n  /* istanbul ignore if */\n  if (isDef(on[CHECKBOX_RADIO_TOKEN])) {\n    on.change = [].concat(on[CHECKBOX_RADIO_TOKEN], on.change || []);\n    delete on[CHECKBOX_RADIO_TOKEN];\n  }\n}\n\nvar target$1;\n\nfunction createOnceHandler (handler, event, capture) {\n  var _target = target$1; // save current target element in closure\n  return function onceHandler () {\n    var res = handler.apply(null, arguments);\n    if (res !== null) {\n      remove$2(event, onceHandler, capture, _target);\n    }\n  }\n}\n\nfunction add$1 (\n  event,\n  handler,\n  once$$1,\n  capture,\n  passive\n) {\n  handler = withMacroTask(handler);\n  if (once$$1) { handler = createOnceHandler(handler, event, capture); }\n  target$1.addEventListener(\n    event,\n    handler,\n    supportsPassive\n      ? { capture: capture, passive: passive }\n      : capture\n  );\n}\n\nfunction remove$2 (\n  event,\n  handler,\n  capture,\n  _target\n) {\n  (_target || target$1).removeEventListener(\n    event,\n    handler._withTask || handler,\n    capture\n  );\n}\n\nfunction updateDOMListeners (oldVnode, vnode) {\n  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {\n    return\n  }\n  var on = vnode.data.on || {};\n  var oldOn = oldVnode.data.on || {};\n  target$1 = vnode.elm;\n  normalizeEvents(on);\n  updateListeners(on, oldOn, add$1, remove$2, vnode.context);\n  target$1 = undefined;\n}\n\nvar events = {\n  create: updateDOMListeners,\n  update: updateDOMListeners\n};\n\n/*  */\n\nfunction updateDOMProps (oldVnode, vnode) {\n  if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {\n    return\n  }\n  var key, cur;\n  var elm = vnode.elm;\n  var oldProps = oldVnode.data.domProps || {};\n  var props = vnode.data.domProps || {};\n  // clone observed objects, as the user probably wants to mutate it\n  if (isDef(props.__ob__)) {\n    props = vnode.data.domProps = extend({}, props);\n  }\n\n  for (key in oldProps) {\n    if (isUndef(props[key])) {\n      elm[key] = '';\n    }\n  }\n  for (key in props) {\n    cur = props[key];\n    // ignore children if the node has textContent or innerHTML,\n    // as these will throw away existing DOM nodes and cause removal errors\n    // on subsequent patches (#3360)\n    if (key === 'textContent' || key === 'innerHTML') {\n      if (vnode.children) { vnode.children.length = 0; }\n      if (cur === oldProps[key]) { continue }\n      // #6601 work around Chrome version <= 55 bug where single textNode\n      // replaced by innerHTML/textContent retains its parentNode property\n      if (elm.childNodes.length === 1) {\n        elm.removeChild(elm.childNodes[0]);\n      }\n    }\n\n    if (key === 'value') {\n      // store value as _value as well since\n      // non-string values will be stringified\n      elm._value = cur;\n      // avoid resetting cursor position when value is the same\n      var strCur = isUndef(cur) ? '' : String(cur);\n      if (shouldUpdateValue(elm, strCur)) {\n        elm.value = strCur;\n      }\n    } else {\n      elm[key] = cur;\n    }\n  }\n}\n\n// check platforms/web/util/attrs.js acceptValue\n\n\nfunction shouldUpdateValue (elm, checkVal) {\n  return (!elm.composing && (\n    elm.tagName === 'OPTION' ||\n    isNotInFocusAndDirty(elm, checkVal) ||\n    isDirtyWithModifiers(elm, checkVal)\n  ))\n}\n\nfunction isNotInFocusAndDirty (elm, checkVal) {\n  // return true when textbox (.number and .trim) loses focus and its value is\n  // not equal to the updated value\n  var notInFocus = true;\n  // #6157\n  // work around IE bug when accessing document.activeElement in an iframe\n  try { notInFocus = document.activeElement !== elm; } catch (e) {}\n  return notInFocus && elm.value !== checkVal\n}\n\nfunction isDirtyWithModifiers (elm, newVal) {\n  var value = elm.value;\n  var modifiers = elm._vModifiers; // injected by v-model runtime\n  if (isDef(modifiers)) {\n    if (modifiers.lazy) {\n      // inputs with lazy should only be updated when not in focus\n      return false\n    }\n    if (modifiers.number) {\n      return toNumber(value) !== toNumber(newVal)\n    }\n    if (modifiers.trim) {\n      return value.trim() !== newVal.trim()\n    }\n  }\n  return value !== newVal\n}\n\nvar domProps = {\n  create: updateDOMProps,\n  update: updateDOMProps\n};\n\n/*  */\n\nvar parseStyleText = cached(function (cssText) {\n  var res = {};\n  var listDelimiter = /;(?![^(]*\\))/g;\n  var propertyDelimiter = /:(.+)/;\n  cssText.split(listDelimiter).forEach(function (item) {\n    if (item) {\n      var tmp = item.split(propertyDelimiter);\n      tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim());\n    }\n  });\n  return res\n});\n\n// merge static and dynamic style data on the same vnode\nfunction normalizeStyleData (data) {\n  var style = normalizeStyleBinding(data.style);\n  // static style is pre-processed into an object during compilation\n  // and is always a fresh object, so it's safe to merge into it\n  return data.staticStyle\n    ? extend(data.staticStyle, style)\n    : style\n}\n\n// normalize possible array / string values into Object\nfunction normalizeStyleBinding (bindingStyle) {\n  if (Array.isArray(bindingStyle)) {\n    return toObject(bindingStyle)\n  }\n  if (typeof bindingStyle === 'string') {\n    return parseStyleText(bindingStyle)\n  }\n  return bindingStyle\n}\n\n/**\n * parent component style should be after child's\n * so that parent component's style could override it\n */\nfunction getStyle (vnode, checkChild) {\n  var res = {};\n  var styleData;\n\n  if (checkChild) {\n    var childNode = vnode;\n    while (childNode.componentInstance) {\n      childNode = childNode.componentInstance._vnode;\n      if (childNode.data && (styleData = normalizeStyleData(childNode.data))) {\n        extend(res, styleData);\n      }\n    }\n  }\n\n  if ((styleData = normalizeStyleData(vnode.data))) {\n    extend(res, styleData);\n  }\n\n  var parentNode = vnode;\n  while ((parentNode = parentNode.parent)) {\n    if (parentNode.data && (styleData = normalizeStyleData(parentNode.data))) {\n      extend(res, styleData);\n    }\n  }\n  return res\n}\n\n/*  */\n\nvar cssVarRE = /^--/;\nvar importantRE = /\\s*!important$/;\nvar setProp = function (el, name, val) {\n  /* istanbul ignore if */\n  if (cssVarRE.test(name)) {\n    el.style.setProperty(name, val);\n  } else if (importantRE.test(val)) {\n    el.style.setProperty(name, val.replace(importantRE, ''), 'important');\n  } else {\n    var normalizedName = normalize(name);\n    if (Array.isArray(val)) {\n      // Support values array created by autoprefixer, e.g.\n      // {display: [\"-webkit-box\", \"-ms-flexbox\", \"flex\"]}\n      // Set them one by one, and the browser will only set those it can recognize\n      for (var i = 0, len = val.length; i < len; i++) {\n        el.style[normalizedName] = val[i];\n      }\n    } else {\n      el.style[normalizedName] = val;\n    }\n  }\n};\n\nvar vendorNames = ['Webkit', 'Moz', 'ms'];\n\nvar emptyStyle;\nvar normalize = cached(function (prop) {\n  emptyStyle = emptyStyle || document.createElement('div').style;\n  prop = camelize(prop);\n  if (prop !== 'filter' && (prop in emptyStyle)) {\n    return prop\n  }\n  var capName = prop.charAt(0).toUpperCase() + prop.slice(1);\n  for (var i = 0; i < vendorNames.length; i++) {\n    var name = vendorNames[i] + capName;\n    if (name in emptyStyle) {\n      return name\n    }\n  }\n});\n\nfunction updateStyle (oldVnode, vnode) {\n  var data = vnode.data;\n  var oldData = oldVnode.data;\n\n  if (isUndef(data.staticStyle) && isUndef(data.style) &&\n    isUndef(oldData.staticStyle) && isUndef(oldData.style)\n  ) {\n    return\n  }\n\n  var cur, name;\n  var el = vnode.elm;\n  var oldStaticStyle = oldData.staticStyle;\n  var oldStyleBinding = oldData.normalizedStyle || oldData.style || {};\n\n  // if static style exists, stylebinding already merged into it when doing normalizeStyleData\n  var oldStyle = oldStaticStyle || oldStyleBinding;\n\n  var style = normalizeStyleBinding(vnode.data.style) || {};\n\n  // store normalized style under a different key for next diff\n  // make sure to clone it if it's reactive, since the user likely wants\n  // to mutate it.\n  vnode.data.normalizedStyle = isDef(style.__ob__)\n    ? extend({}, style)\n    : style;\n\n  var newStyle = getStyle(vnode, true);\n\n  for (name in oldStyle) {\n    if (isUndef(newStyle[name])) {\n      setProp(el, name, '');\n    }\n  }\n  for (name in newStyle) {\n    cur = newStyle[name];\n    if (cur !== oldStyle[name]) {\n      // ie9 setting to null has no effect, must use empty string\n      setProp(el, name, cur == null ? '' : cur);\n    }\n  }\n}\n\nvar style = {\n  create: updateStyle,\n  update: updateStyle\n};\n\n/*  */\n\n/**\n * Add class with compatibility for SVG since classList is not supported on\n * SVG elements in IE\n */\nfunction addClass (el, cls) {\n  /* istanbul ignore if */\n  if (!cls || !(cls = cls.trim())) {\n    return\n  }\n\n  /* istanbul ignore else */\n  if (el.classList) {\n    if (cls.indexOf(' ') > -1) {\n      cls.split(/\\s+/).forEach(function (c) { return el.classList.add(c); });\n    } else {\n      el.classList.add(cls);\n    }\n  } else {\n    var cur = \" \" + (el.getAttribute('class') || '') + \" \";\n    if (cur.indexOf(' ' + cls + ' ') < 0) {\n      el.setAttribute('class', (cur + cls).trim());\n    }\n  }\n}\n\n/**\n * Remove class with compatibility for SVG since classList is not supported on\n * SVG elements in IE\n */\nfunction removeClass (el, cls) {\n  /* istanbul ignore if */\n  if (!cls || !(cls = cls.trim())) {\n    return\n  }\n\n  /* istanbul ignore else */\n  if (el.classList) {\n    if (cls.indexOf(' ') > -1) {\n      cls.split(/\\s+/).forEach(function (c) { return el.classList.remove(c); });\n    } else {\n      el.classList.remove(cls);\n    }\n    if (!el.classList.length) {\n      el.removeAttribute('class');\n    }\n  } else {\n    var cur = \" \" + (el.getAttribute('class') || '') + \" \";\n    var tar = ' ' + cls + ' ';\n    while (cur.indexOf(tar) >= 0) {\n      cur = cur.replace(tar, ' ');\n    }\n    cur = cur.trim();\n    if (cur) {\n      el.setAttribute('class', cur);\n    } else {\n      el.removeAttribute('class');\n    }\n  }\n}\n\n/*  */\n\nfunction resolveTransition (def) {\n  if (!def) {\n    return\n  }\n  /* istanbul ignore else */\n  if (typeof def === 'object') {\n    var res = {};\n    if (def.css !== false) {\n      extend(res, autoCssTransition(def.name || 'v'));\n    }\n    extend(res, def);\n    return res\n  } else if (typeof def === 'string') {\n    return autoCssTransition(def)\n  }\n}\n\nvar autoCssTransition = cached(function (name) {\n  return {\n    enterClass: (name + \"-enter\"),\n    enterToClass: (name + \"-enter-to\"),\n    enterActiveClass: (name + \"-enter-active\"),\n    leaveClass: (name + \"-leave\"),\n    leaveToClass: (name + \"-leave-to\"),\n    leaveActiveClass: (name + \"-leave-active\")\n  }\n});\n\nvar hasTransition = inBrowser && !isIE9;\nvar TRANSITION = 'transition';\nvar ANIMATION = 'animation';\n\n// Transition property/event sniffing\nvar transitionProp = 'transition';\nvar transitionEndEvent = 'transitionend';\nvar animationProp = 'animation';\nvar animationEndEvent = 'animationend';\nif (hasTransition) {\n  /* istanbul ignore if */\n  if (window.ontransitionend === undefined &&\n    window.onwebkittransitionend !== undefined\n  ) {\n    transitionProp = 'WebkitTransition';\n    transitionEndEvent = 'webkitTransitionEnd';\n  }\n  if (window.onanimationend === undefined &&\n    window.onwebkitanimationend !== undefined\n  ) {\n    animationProp = 'WebkitAnimation';\n    animationEndEvent = 'webkitAnimationEnd';\n  }\n}\n\n// binding to window is necessary to make hot reload work in IE in strict mode\nvar raf = inBrowser\n  ? window.requestAnimationFrame\n    ? window.requestAnimationFrame.bind(window)\n    : setTimeout\n  : /* istanbul ignore next */ function (fn) { return fn(); };\n\nfunction nextFrame (fn) {\n  raf(function () {\n    raf(fn);\n  });\n}\n\nfunction addTransitionClass (el, cls) {\n  var transitionClasses = el._transitionClasses || (el._transitionClasses = []);\n  if (transitionClasses.indexOf(cls) < 0) {\n    transitionClasses.push(cls);\n    addClass(el, cls);\n  }\n}\n\nfunction removeTransitionClass (el, cls) {\n  if (el._transitionClasses) {\n    remove(el._transitionClasses, cls);\n  }\n  removeClass(el, cls);\n}\n\nfunction whenTransitionEnds (\n  el,\n  expectedType,\n  cb\n) {\n  var ref = getTransitionInfo(el, expectedType);\n  var type = ref.type;\n  var timeout = ref.timeout;\n  var propCount = ref.propCount;\n  if (!type) { return cb() }\n  var event = type === TRANSITION ? transitionEndEvent : animationEndEvent;\n  var ended = 0;\n  var end = function () {\n    el.removeEventListener(event, onEnd);\n    cb();\n  };\n  var onEnd = function (e) {\n    if (e.target === el) {\n      if (++ended >= propCount) {\n        end();\n      }\n    }\n  };\n  setTimeout(function () {\n    if (ended < propCount) {\n      end();\n    }\n  }, timeout + 1);\n  el.addEventListener(event, onEnd);\n}\n\nvar transformRE = /\\b(transform|all)(,|$)/;\n\nfunction getTransitionInfo (el, expectedType) {\n  var styles = window.getComputedStyle(el);\n  var transitionDelays = styles[transitionProp + 'Delay'].split(', ');\n  var transitionDurations = styles[transitionProp + 'Duration'].split(', ');\n  var transitionTimeout = getTimeout(transitionDelays, transitionDurations);\n  var animationDelays = styles[animationProp + 'Delay'].split(', ');\n  var animationDurations = styles[animationProp + 'Duration'].split(', ');\n  var animationTimeout = getTimeout(animationDelays, animationDurations);\n\n  var type;\n  var timeout = 0;\n  var propCount = 0;\n  /* istanbul ignore if */\n  if (expectedType === TRANSITION) {\n    if (transitionTimeout > 0) {\n      type = TRANSITION;\n      timeout = transitionTimeout;\n      propCount = transitionDurations.length;\n    }\n  } else if (expectedType === ANIMATION) {\n    if (animationTimeout > 0) {\n      type = ANIMATION;\n      timeout = animationTimeout;\n      propCount = animationDurations.length;\n    }\n  } else {\n    timeout = Math.max(transitionTimeout, animationTimeout);\n    type = timeout > 0\n      ? transitionTimeout > animationTimeout\n        ? TRANSITION\n        : ANIMATION\n      : null;\n    propCount = type\n      ? type === TRANSITION\n        ? transitionDurations.length\n        : animationDurations.length\n      : 0;\n  }\n  var hasTransform =\n    type === TRANSITION &&\n    transformRE.test(styles[transitionProp + 'Property']);\n  return {\n    type: type,\n    timeout: timeout,\n    propCount: propCount,\n    hasTransform: hasTransform\n  }\n}\n\nfunction getTimeout (delays, durations) {\n  /* istanbul ignore next */\n  while (delays.length < durations.length) {\n    delays = delays.concat(delays);\n  }\n\n  return Math.max.apply(null, durations.map(function (d, i) {\n    return toMs(d) + toMs(delays[i])\n  }))\n}\n\nfunction toMs (s) {\n  return Number(s.slice(0, -1)) * 1000\n}\n\n/*  */\n\nfunction enter (vnode, toggleDisplay) {\n  var el = vnode.elm;\n\n  // call leave callback now\n  if (isDef(el._leaveCb)) {\n    el._leaveCb.cancelled = true;\n    el._leaveCb();\n  }\n\n  var data = resolveTransition(vnode.data.transition);\n  if (isUndef(data)) {\n    return\n  }\n\n  /* istanbul ignore if */\n  if (isDef(el._enterCb) || el.nodeType !== 1) {\n    return\n  }\n\n  var css = data.css;\n  var type = data.type;\n  var enterClass = data.enterClass;\n  var enterToClass = data.enterToClass;\n  var enterActiveClass = data.enterActiveClass;\n  var appearClass = data.appearClass;\n  var appearToClass = data.appearToClass;\n  var appearActiveClass = data.appearActiveClass;\n  var beforeEnter = data.beforeEnter;\n  var enter = data.enter;\n  var afterEnter = data.afterEnter;\n  var enterCancelled = data.enterCancelled;\n  var beforeAppear = data.beforeAppear;\n  var appear = data.appear;\n  var afterAppear = data.afterAppear;\n  var appearCancelled = data.appearCancelled;\n  var duration = data.duration;\n\n  // activeInstance will always be the <transition> component managing this\n  // transition. One edge case to check is when the <transition> is placed\n  // as the root node of a child component. In that case we need to check\n  // <transition>'s parent for appear check.\n  var context = activeInstance;\n  var transitionNode = activeInstance.$vnode;\n  while (transitionNode && transitionNode.parent) {\n    transitionNode = transitionNode.parent;\n    context = transitionNode.context;\n  }\n\n  var isAppear = !context._isMounted || !vnode.isRootInsert;\n\n  if (isAppear && !appear && appear !== '') {\n    return\n  }\n\n  var startClass = isAppear && appearClass\n    ? appearClass\n    : enterClass;\n  var activeClass = isAppear && appearActiveClass\n    ? appearActiveClass\n    : enterActiveClass;\n  var toClass = isAppear && appearToClass\n    ? appearToClass\n    : enterToClass;\n\n  var beforeEnterHook = isAppear\n    ? (beforeAppear || beforeEnter)\n    : beforeEnter;\n  var enterHook = isAppear\n    ? (typeof appear === 'function' ? appear : enter)\n    : enter;\n  var afterEnterHook = isAppear\n    ? (afterAppear || afterEnter)\n    : afterEnter;\n  var enterCancelledHook = isAppear\n    ? (appearCancelled || enterCancelled)\n    : enterCancelled;\n\n  var explicitEnterDuration = toNumber(\n    isObject(duration)\n      ? duration.enter\n      : duration\n  );\n\n  if (\"development\" !== 'production' && explicitEnterDuration != null) {\n    checkDuration(explicitEnterDuration, 'enter', vnode);\n  }\n\n  var expectsCSS = css !== false && !isIE9;\n  var userWantsControl = getHookArgumentsLength(enterHook);\n\n  var cb = el._enterCb = once(function () {\n    if (expectsCSS) {\n      removeTransitionClass(el, toClass);\n      removeTransitionClass(el, activeClass);\n    }\n    if (cb.cancelled) {\n      if (expectsCSS) {\n        removeTransitionClass(el, startClass);\n      }\n      enterCancelledHook && enterCancelledHook(el);\n    } else {\n      afterEnterHook && afterEnterHook(el);\n    }\n    el._enterCb = null;\n  });\n\n  if (!vnode.data.show) {\n    // remove pending leave element on enter by injecting an insert hook\n    mergeVNodeHook(vnode, 'insert', function () {\n      var parent = el.parentNode;\n      var pendingNode = parent && parent._pending && parent._pending[vnode.key];\n      if (pendingNode &&\n        pendingNode.tag === vnode.tag &&\n        pendingNode.elm._leaveCb\n      ) {\n        pendingNode.elm._leaveCb();\n      }\n      enterHook && enterHook(el, cb);\n    });\n  }\n\n  // start enter transition\n  beforeEnterHook && beforeEnterHook(el);\n  if (expectsCSS) {\n    addTransitionClass(el, startClass);\n    addTransitionClass(el, activeClass);\n    nextFrame(function () {\n      addTransitionClass(el, toClass);\n      removeTransitionClass(el, startClass);\n      if (!cb.cancelled && !userWantsControl) {\n        if (isValidDuration(explicitEnterDuration)) {\n          setTimeout(cb, explicitEnterDuration);\n        } else {\n          whenTransitionEnds(el, type, cb);\n        }\n      }\n    });\n  }\n\n  if (vnode.data.show) {\n    toggleDisplay && toggleDisplay();\n    enterHook && enterHook(el, cb);\n  }\n\n  if (!expectsCSS && !userWantsControl) {\n    cb();\n  }\n}\n\nfunction leave (vnode, rm) {\n  var el = vnode.elm;\n\n  // call enter callback now\n  if (isDef(el._enterCb)) {\n    el._enterCb.cancelled = true;\n    el._enterCb();\n  }\n\n  var data = resolveTransition(vnode.data.transition);\n  if (isUndef(data) || el.nodeType !== 1) {\n    return rm()\n  }\n\n  /* istanbul ignore if */\n  if (isDef(el._leaveCb)) {\n    return\n  }\n\n  var css = data.css;\n  var type = data.type;\n  var leaveClass = data.leaveClass;\n  var leaveToClass = data.leaveToClass;\n  var leaveActiveClass = data.leaveActiveClass;\n  var beforeLeave = data.beforeLeave;\n  var leave = data.leave;\n  var afterLeave = data.afterLeave;\n  var leaveCancelled = data.leaveCancelled;\n  var delayLeave = data.delayLeave;\n  var duration = data.duration;\n\n  var expectsCSS = css !== false && !isIE9;\n  var userWantsControl = getHookArgumentsLength(leave);\n\n  var explicitLeaveDuration = toNumber(\n    isObject(duration)\n      ? duration.leave\n      : duration\n  );\n\n  if (\"development\" !== 'production' && isDef(explicitLeaveDuration)) {\n    checkDuration(explicitLeaveDuration, 'leave', vnode);\n  }\n\n  var cb = el._leaveCb = once(function () {\n    if (el.parentNode && el.parentNode._pending) {\n      el.parentNode._pending[vnode.key] = null;\n    }\n    if (expectsCSS) {\n      removeTransitionClass(el, leaveToClass);\n      removeTransitionClass(el, leaveActiveClass);\n    }\n    if (cb.cancelled) {\n      if (expectsCSS) {\n        removeTransitionClass(el, leaveClass);\n      }\n      leaveCancelled && leaveCancelled(el);\n    } else {\n      rm();\n      afterLeave && afterLeave(el);\n    }\n    el._leaveCb = null;\n  });\n\n  if (delayLeave) {\n    delayLeave(performLeave);\n  } else {\n    performLeave();\n  }\n\n  function performLeave () {\n    // the delayed leave may have already been cancelled\n    if (cb.cancelled) {\n      return\n    }\n    // record leaving element\n    if (!vnode.data.show) {\n      (el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key)] = vnode;\n    }\n    beforeLeave && beforeLeave(el);\n    if (expectsCSS) {\n      addTransitionClass(el, leaveClass);\n      addTransitionClass(el, leaveActiveClass);\n      nextFrame(function () {\n        addTransitionClass(el, leaveToClass);\n        removeTransitionClass(el, leaveClass);\n        if (!cb.cancelled && !userWantsControl) {\n          if (isValidDuration(explicitLeaveDuration)) {\n            setTimeout(cb, explicitLeaveDuration);\n          } else {\n            whenTransitionEnds(el, type, cb);\n          }\n        }\n      });\n    }\n    leave && leave(el, cb);\n    if (!expectsCSS && !userWantsControl) {\n      cb();\n    }\n  }\n}\n\n// only used in dev mode\nfunction checkDuration (val, name, vnode) {\n  if (typeof val !== 'number') {\n    warn(\n      \"<transition> explicit \" + name + \" duration is not a valid number - \" +\n      \"got \" + (JSON.stringify(val)) + \".\",\n      vnode.context\n    );\n  } else if (isNaN(val)) {\n    warn(\n      \"<transition> explicit \" + name + \" duration is NaN - \" +\n      'the duration expression might be incorrect.',\n      vnode.context\n    );\n  }\n}\n\nfunction isValidDuration (val) {\n  return typeof val === 'number' && !isNaN(val)\n}\n\n/**\n * Normalize a transition hook's argument length. The hook may be:\n * - a merged hook (invoker) with the original in .fns\n * - a wrapped component method (check ._length)\n * - a plain function (.length)\n */\nfunction getHookArgumentsLength (fn) {\n  if (isUndef(fn)) {\n    return false\n  }\n  var invokerFns = fn.fns;\n  if (isDef(invokerFns)) {\n    // invoker\n    return getHookArgumentsLength(\n      Array.isArray(invokerFns)\n        ? invokerFns[0]\n        : invokerFns\n    )\n  } else {\n    return (fn._length || fn.length) > 1\n  }\n}\n\nfunction _enter (_, vnode) {\n  if (vnode.data.show !== true) {\n    enter(vnode);\n  }\n}\n\nvar transition = inBrowser ? {\n  create: _enter,\n  activate: _enter,\n  remove: function remove$$1 (vnode, rm) {\n    /* istanbul ignore else */\n    if (vnode.data.show !== true) {\n      leave(vnode, rm);\n    } else {\n      rm();\n    }\n  }\n} : {};\n\nvar platformModules = [\n  attrs,\n  klass,\n  events,\n  domProps,\n  style,\n  transition\n];\n\n/*  */\n\n// the directive module should be applied last, after all\n// built-in modules have been applied.\nvar modules = platformModules.concat(baseModules);\n\nvar patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });\n\n/**\n * Not type checking this file because flow doesn't like attaching\n * properties to Elements.\n */\n\n/* istanbul ignore if */\nif (isIE9) {\n  // http://www.matts411.com/post/internet-explorer-9-oninput/\n  document.addEventListener('selectionchange', function () {\n    var el = document.activeElement;\n    if (el && el.vmodel) {\n      trigger(el, 'input');\n    }\n  });\n}\n\nvar directive = {\n  inserted: function inserted (el, binding, vnode, oldVnode) {\n    if (vnode.tag === 'select') {\n      // #6903\n      if (oldVnode.elm && !oldVnode.elm._vOptions) {\n        mergeVNodeHook(vnode, 'postpatch', function () {\n          directive.componentUpdated(el, binding, vnode);\n        });\n      } else {\n        setSelected(el, binding, vnode.context);\n      }\n      el._vOptions = [].map.call(el.options, getValue);\n    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {\n      el._vModifiers = binding.modifiers;\n      if (!binding.modifiers.lazy) {\n        // Safari < 10.2 & UIWebView doesn't fire compositionend when\n        // switching focus before confirming composition choice\n        // this also fixes the issue where some browsers e.g. iOS Chrome\n        // fires \"change\" instead of \"input\" on autocomplete.\n        el.addEventListener('change', onCompositionEnd);\n        if (!isAndroid) {\n          el.addEventListener('compositionstart', onCompositionStart);\n          el.addEventListener('compositionend', onCompositionEnd);\n        }\n        /* istanbul ignore if */\n        if (isIE9) {\n          el.vmodel = true;\n        }\n      }\n    }\n  },\n\n  componentUpdated: function componentUpdated (el, binding, vnode) {\n    if (vnode.tag === 'select') {\n      setSelected(el, binding, vnode.context);\n      // in case the options rendered by v-for have changed,\n      // it's possible that the value is out-of-sync with the rendered options.\n      // detect such cases and filter out values that no longer has a matching\n      // option in the DOM.\n      var prevOptions = el._vOptions;\n      var curOptions = el._vOptions = [].map.call(el.options, getValue);\n      if (curOptions.some(function (o, i) { return !looseEqual(o, prevOptions[i]); })) {\n        // trigger change event if\n        // no matching option found for at least one value\n        var needReset = el.multiple\n          ? binding.value.some(function (v) { return hasNoMatchingOption(v, curOptions); })\n          : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions);\n        if (needReset) {\n          trigger(el, 'change');\n        }\n      }\n    }\n  }\n};\n\nfunction setSelected (el, binding, vm) {\n  actuallySetSelected(el, binding, vm);\n  /* istanbul ignore if */\n  if (isIE || isEdge) {\n    setTimeout(function () {\n      actuallySetSelected(el, binding, vm);\n    }, 0);\n  }\n}\n\nfunction actuallySetSelected (el, binding, vm) {\n  var value = binding.value;\n  var isMultiple = el.multiple;\n  if (isMultiple && !Array.isArray(value)) {\n    \"development\" !== 'production' && warn(\n      \"<select multiple v-model=\\\"\" + (binding.expression) + \"\\\"> \" +\n      \"expects an Array value for its binding, but got \" + (Object.prototype.toString.call(value).slice(8, -1)),\n      vm\n    );\n    return\n  }\n  var selected, option;\n  for (var i = 0, l = el.options.length; i < l; i++) {\n    option = el.options[i];\n    if (isMultiple) {\n      selected = looseIndexOf(value, getValue(option)) > -1;\n      if (option.selected !== selected) {\n        option.selected = selected;\n      }\n    } else {\n      if (looseEqual(getValue(option), value)) {\n        if (el.selectedIndex !== i) {\n          el.selectedIndex = i;\n        }\n        return\n      }\n    }\n  }\n  if (!isMultiple) {\n    el.selectedIndex = -1;\n  }\n}\n\nfunction hasNoMatchingOption (value, options) {\n  return options.every(function (o) { return !looseEqual(o, value); })\n}\n\nfunction getValue (option) {\n  return '_value' in option\n    ? option._value\n    : option.value\n}\n\nfunction onCompositionStart (e) {\n  e.target.composing = true;\n}\n\nfunction onCompositionEnd (e) {\n  // prevent triggering an input event for no reason\n  if (!e.target.composing) { return }\n  e.target.composing = false;\n  trigger(e.target, 'input');\n}\n\nfunction trigger (el, type) {\n  var e = document.createEvent('HTMLEvents');\n  e.initEvent(type, true, true);\n  el.dispatchEvent(e);\n}\n\n/*  */\n\n// recursively search for possible transition defined inside the component root\nfunction locateNode (vnode) {\n  return vnode.componentInstance && (!vnode.data || !vnode.data.transition)\n    ? locateNode(vnode.componentInstance._vnode)\n    : vnode\n}\n\nvar show = {\n  bind: function bind (el, ref, vnode) {\n    var value = ref.value;\n\n    vnode = locateNode(vnode);\n    var transition$$1 = vnode.data && vnode.data.transition;\n    var originalDisplay = el.__vOriginalDisplay =\n      el.style.display === 'none' ? '' : el.style.display;\n    if (value && transition$$1) {\n      vnode.data.show = true;\n      enter(vnode, function () {\n        el.style.display = originalDisplay;\n      });\n    } else {\n      el.style.display = value ? originalDisplay : 'none';\n    }\n  },\n\n  update: function update (el, ref, vnode) {\n    var value = ref.value;\n    var oldValue = ref.oldValue;\n\n    /* istanbul ignore if */\n    if (value === oldValue) { return }\n    vnode = locateNode(vnode);\n    var transition$$1 = vnode.data && vnode.data.transition;\n    if (transition$$1) {\n      vnode.data.show = true;\n      if (value) {\n        enter(vnode, function () {\n          el.style.display = el.__vOriginalDisplay;\n        });\n      } else {\n        leave(vnode, function () {\n          el.style.display = 'none';\n        });\n      }\n    } else {\n      el.style.display = value ? el.__vOriginalDisplay : 'none';\n    }\n  },\n\n  unbind: function unbind (\n    el,\n    binding,\n    vnode,\n    oldVnode,\n    isDestroy\n  ) {\n    if (!isDestroy) {\n      el.style.display = el.__vOriginalDisplay;\n    }\n  }\n};\n\nvar platformDirectives = {\n  model: directive,\n  show: show\n};\n\n/*  */\n\n// Provides transition support for a single element/component.\n// supports transition mode (out-in / in-out)\n\nvar transitionProps = {\n  name: String,\n  appear: Boolean,\n  css: Boolean,\n  mode: String,\n  type: String,\n  enterClass: String,\n  leaveClass: String,\n  enterToClass: String,\n  leaveToClass: String,\n  enterActiveClass: String,\n  leaveActiveClass: String,\n  appearClass: String,\n  appearActiveClass: String,\n  appearToClass: String,\n  duration: [Number, String, Object]\n};\n\n// in case the child is also an abstract component, e.g. <keep-alive>\n// we want to recursively retrieve the real component to be rendered\nfunction getRealChild (vnode) {\n  var compOptions = vnode && vnode.componentOptions;\n  if (compOptions && compOptions.Ctor.options.abstract) {\n    return getRealChild(getFirstComponentChild(compOptions.children))\n  } else {\n    return vnode\n  }\n}\n\nfunction extractTransitionData (comp) {\n  var data = {};\n  var options = comp.$options;\n  // props\n  for (var key in options.propsData) {\n    data[key] = comp[key];\n  }\n  // events.\n  // extract listeners and pass them directly to the transition methods\n  var listeners = options._parentListeners;\n  for (var key$1 in listeners) {\n    data[camelize(key$1)] = listeners[key$1];\n  }\n  return data\n}\n\nfunction placeholder (h, rawChild) {\n  if (/\\d-keep-alive$/.test(rawChild.tag)) {\n    return h('keep-alive', {\n      props: rawChild.componentOptions.propsData\n    })\n  }\n}\n\nfunction hasParentTransition (vnode) {\n  while ((vnode = vnode.parent)) {\n    if (vnode.data.transition) {\n      return true\n    }\n  }\n}\n\nfunction isSameChild (child, oldChild) {\n  return oldChild.key === child.key && oldChild.tag === child.tag\n}\n\nvar Transition = {\n  name: 'transition',\n  props: transitionProps,\n  abstract: true,\n\n  render: function render (h) {\n    var this$1 = this;\n\n    var children = this.$slots.default;\n    if (!children) {\n      return\n    }\n\n    // filter out text nodes (possible whitespaces)\n    children = children.filter(function (c) { return c.tag || isAsyncPlaceholder(c); });\n    /* istanbul ignore if */\n    if (!children.length) {\n      return\n    }\n\n    // warn multiple elements\n    if (\"development\" !== 'production' && children.length > 1) {\n      warn(\n        '<transition> can only be used on a single element. Use ' +\n        '<transition-group> for lists.',\n        this.$parent\n      );\n    }\n\n    var mode = this.mode;\n\n    // warn invalid mode\n    if (\"development\" !== 'production' &&\n      mode && mode !== 'in-out' && mode !== 'out-in'\n    ) {\n      warn(\n        'invalid <transition> mode: ' + mode,\n        this.$parent\n      );\n    }\n\n    var rawChild = children[0];\n\n    // if this is a component root node and the component's\n    // parent container node also has transition, skip.\n    if (hasParentTransition(this.$vnode)) {\n      return rawChild\n    }\n\n    // apply transition data to child\n    // use getRealChild() to ignore abstract components e.g. keep-alive\n    var child = getRealChild(rawChild);\n    /* istanbul ignore if */\n    if (!child) {\n      return rawChild\n    }\n\n    if (this._leaving) {\n      return placeholder(h, rawChild)\n    }\n\n    // ensure a key that is unique to the vnode type and to this transition\n    // component instance. This key will be used to remove pending leaving nodes\n    // during entering.\n    var id = \"__transition-\" + (this._uid) + \"-\";\n    child.key = child.key == null\n      ? child.isComment\n        ? id + 'comment'\n        : id + child.tag\n      : isPrimitive(child.key)\n        ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)\n        : child.key;\n\n    var data = (child.data || (child.data = {})).transition = extractTransitionData(this);\n    var oldRawChild = this._vnode;\n    var oldChild = getRealChild(oldRawChild);\n\n    // mark v-show\n    // so that the transition module can hand over the control to the directive\n    if (child.data.directives && child.data.directives.some(function (d) { return d.name === 'show'; })) {\n      child.data.show = true;\n    }\n\n    if (\n      oldChild &&\n      oldChild.data &&\n      !isSameChild(child, oldChild) &&\n      !isAsyncPlaceholder(oldChild) &&\n      // #6687 component root is a comment node\n      !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)\n    ) {\n      // replace old child transition data with fresh one\n      // important for dynamic transitions!\n      var oldData = oldChild.data.transition = extend({}, data);\n      // handle transition mode\n      if (mode === 'out-in') {\n        // return placeholder node and queue update when leave finishes\n        this._leaving = true;\n        mergeVNodeHook(oldData, 'afterLeave', function () {\n          this$1._leaving = false;\n          this$1.$forceUpdate();\n        });\n        return placeholder(h, rawChild)\n      } else if (mode === 'in-out') {\n        if (isAsyncPlaceholder(child)) {\n          return oldRawChild\n        }\n        var delayedLeave;\n        var performLeave = function () { delayedLeave(); };\n        mergeVNodeHook(data, 'afterEnter', performLeave);\n        mergeVNodeHook(data, 'enterCancelled', performLeave);\n        mergeVNodeHook(oldData, 'delayLeave', function (leave) { delayedLeave = leave; });\n      }\n    }\n\n    return rawChild\n  }\n};\n\n/*  */\n\n// Provides transition support for list items.\n// supports move transitions using the FLIP technique.\n\n// Because the vdom's children update algorithm is \"unstable\" - i.e.\n// it doesn't guarantee the relative positioning of removed elements,\n// we force transition-group to update its children into two passes:\n// in the first pass, we remove all nodes that need to be removed,\n// triggering their leaving transition; in the second pass, we insert/move\n// into the final desired state. This way in the second pass removed\n// nodes will remain where they should be.\n\nvar props = extend({\n  tag: String,\n  moveClass: String\n}, transitionProps);\n\ndelete props.mode;\n\nvar TransitionGroup = {\n  props: props,\n\n  render: function render (h) {\n    var tag = this.tag || this.$vnode.data.tag || 'span';\n    var map = Object.create(null);\n    var prevChildren = this.prevChildren = this.children;\n    var rawChildren = this.$slots.default || [];\n    var children = this.children = [];\n    var transitionData = extractTransitionData(this);\n\n    for (var i = 0; i < rawChildren.length; i++) {\n      var c = rawChildren[i];\n      if (c.tag) {\n        if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {\n          children.push(c);\n          map[c.key] = c\n          ;(c.data || (c.data = {})).transition = transitionData;\n        } else {\n          var opts = c.componentOptions;\n          var name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag;\n          warn((\"<transition-group> children must be keyed: <\" + name + \">\"));\n        }\n      }\n    }\n\n    if (prevChildren) {\n      var kept = [];\n      var removed = [];\n      for (var i$1 = 0; i$1 < prevChildren.length; i$1++) {\n        var c$1 = prevChildren[i$1];\n        c$1.data.transition = transitionData;\n        c$1.data.pos = c$1.elm.getBoundingClientRect();\n        if (map[c$1.key]) {\n          kept.push(c$1);\n        } else {\n          removed.push(c$1);\n        }\n      }\n      this.kept = h(tag, null, kept);\n      this.removed = removed;\n    }\n\n    return h(tag, null, children)\n  },\n\n  beforeUpdate: function beforeUpdate () {\n    // force removing pass\n    this.__patch__(\n      this._vnode,\n      this.kept,\n      false, // hydrating\n      true // removeOnly (!important, avoids unnecessary moves)\n    );\n    this._vnode = this.kept;\n  },\n\n  updated: function updated () {\n    var children = this.prevChildren;\n    var moveClass = this.moveClass || ((this.name || 'v') + '-move');\n    if (!children.length || !this.hasMove(children[0].elm, moveClass)) {\n      return\n    }\n\n    // we divide the work into three loops to avoid mixing DOM reads and writes\n    // in each iteration - which helps prevent layout thrashing.\n    children.forEach(callPendingCbs);\n    children.forEach(recordPosition);\n    children.forEach(applyTranslation);\n\n    // force reflow to put everything in position\n    // assign to this to avoid being removed in tree-shaking\n    // $flow-disable-line\n    this._reflow = document.body.offsetHeight;\n\n    children.forEach(function (c) {\n      if (c.data.moved) {\n        var el = c.elm;\n        var s = el.style;\n        addTransitionClass(el, moveClass);\n        s.transform = s.WebkitTransform = s.transitionDuration = '';\n        el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {\n          if (!e || /transform$/.test(e.propertyName)) {\n            el.removeEventListener(transitionEndEvent, cb);\n            el._moveCb = null;\n            removeTransitionClass(el, moveClass);\n          }\n        });\n      }\n    });\n  },\n\n  methods: {\n    hasMove: function hasMove (el, moveClass) {\n      /* istanbul ignore if */\n      if (!hasTransition) {\n        return false\n      }\n      /* istanbul ignore if */\n      if (this._hasMove) {\n        return this._hasMove\n      }\n      // Detect whether an element with the move class applied has\n      // CSS transitions. Since the element may be inside an entering\n      // transition at this very moment, we make a clone of it and remove\n      // all other transition classes applied to ensure only the move class\n      // is applied.\n      var clone = el.cloneNode();\n      if (el._transitionClasses) {\n        el._transitionClasses.forEach(function (cls) { removeClass(clone, cls); });\n      }\n      addClass(clone, moveClass);\n      clone.style.display = 'none';\n      this.$el.appendChild(clone);\n      var info = getTransitionInfo(clone);\n      this.$el.removeChild(clone);\n      return (this._hasMove = info.hasTransform)\n    }\n  }\n};\n\nfunction callPendingCbs (c) {\n  /* istanbul ignore if */\n  if (c.elm._moveCb) {\n    c.elm._moveCb();\n  }\n  /* istanbul ignore if */\n  if (c.elm._enterCb) {\n    c.elm._enterCb();\n  }\n}\n\nfunction recordPosition (c) {\n  c.data.newPos = c.elm.getBoundingClientRect();\n}\n\nfunction applyTranslation (c) {\n  var oldPos = c.data.pos;\n  var newPos = c.data.newPos;\n  var dx = oldPos.left - newPos.left;\n  var dy = oldPos.top - newPos.top;\n  if (dx || dy) {\n    c.data.moved = true;\n    var s = c.elm.style;\n    s.transform = s.WebkitTransform = \"translate(\" + dx + \"px,\" + dy + \"px)\";\n    s.transitionDuration = '0s';\n  }\n}\n\nvar platformComponents = {\n  Transition: Transition,\n  TransitionGroup: TransitionGroup\n};\n\n/*  */\n\n// install platform specific utils\nVue$3.config.mustUseProp = mustUseProp;\nVue$3.config.isReservedTag = isReservedTag;\nVue$3.config.isReservedAttr = isReservedAttr;\nVue$3.config.getTagNamespace = getTagNamespace;\nVue$3.config.isUnknownElement = isUnknownElement;\n\n// install platform runtime directives & components\nextend(Vue$3.options.directives, platformDirectives);\nextend(Vue$3.options.components, platformComponents);\n\n// install platform patch function\nVue$3.prototype.__patch__ = inBrowser ? patch : noop;\n\n// public mount method\nVue$3.prototype.$mount = function (\n  el,\n  hydrating\n) {\n  el = el && inBrowser ? query(el) : undefined;\n  return mountComponent(this, el, hydrating)\n};\n\n// devtools global hook\n/* istanbul ignore next */\nVue$3.nextTick(function () {\n  if (config.devtools) {\n    if (devtools) {\n      devtools.emit('init', Vue$3);\n    } else if (\"development\" !== 'production' && isChrome) {\n      console[console.info ? 'info' : 'log'](\n        'Download the Vue Devtools extension for a better development experience:\\n' +\n        'https://github.com/vuejs/vue-devtools'\n      );\n    }\n  }\n  if (\"development\" !== 'production' &&\n    config.productionTip !== false &&\n    inBrowser && typeof console !== 'undefined'\n  ) {\n    console[console.info ? 'info' : 'log'](\n      \"You are running Vue in development mode.\\n\" +\n      \"Make sure to turn on production mode when deploying for production.\\n\" +\n      \"See more tips at https://vuejs.org/guide/deployment.html\"\n    );\n  }\n}, 0);\n\n/*  */\n\nvar defaultTagRE = /\\{\\{((?:.|\\n)+?)\\}\\}/g;\nvar regexEscapeRE = /[-.*+?^${}()|[\\]\\/\\\\]/g;\n\nvar buildRegex = cached(function (delimiters) {\n  var open = delimiters[0].replace(regexEscapeRE, '\\\\$&');\n  var close = delimiters[1].replace(regexEscapeRE, '\\\\$&');\n  return new RegExp(open + '((?:.|\\\\n)+?)' + close, 'g')\n});\n\nfunction parseText (\n  text,\n  delimiters\n) {\n  var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;\n  if (!tagRE.test(text)) {\n    return\n  }\n  var tokens = [];\n  var lastIndex = tagRE.lastIndex = 0;\n  var match, index;\n  while ((match = tagRE.exec(text))) {\n    index = match.index;\n    // push text token\n    if (index > lastIndex) {\n      tokens.push(JSON.stringify(text.slice(lastIndex, index)));\n    }\n    // tag token\n    var exp = parseFilters(match[1].trim());\n    tokens.push((\"_s(\" + exp + \")\"));\n    lastIndex = index + match[0].length;\n  }\n  if (lastIndex < text.length) {\n    tokens.push(JSON.stringify(text.slice(lastIndex)));\n  }\n  return tokens.join('+')\n}\n\n/*  */\n\nfunction transformNode (el, options) {\n  var warn = options.warn || baseWarn;\n  var staticClass = getAndRemoveAttr(el, 'class');\n  if (\"development\" !== 'production' && staticClass) {\n    var expression = parseText(staticClass, options.delimiters);\n    if (expression) {\n      warn(\n        \"class=\\\"\" + staticClass + \"\\\": \" +\n        'Interpolation inside attributes has been removed. ' +\n        'Use v-bind or the colon shorthand instead. For example, ' +\n        'instead of <div class=\"{{ val }}\">, use <div :class=\"val\">.'\n      );\n    }\n  }\n  if (staticClass) {\n    el.staticClass = JSON.stringify(staticClass);\n  }\n  var classBinding = getBindingAttr(el, 'class', false /* getStatic */);\n  if (classBinding) {\n    el.classBinding = classBinding;\n  }\n}\n\nfunction genData (el) {\n  var data = '';\n  if (el.staticClass) {\n    data += \"staticClass:\" + (el.staticClass) + \",\";\n  }\n  if (el.classBinding) {\n    data += \"class:\" + (el.classBinding) + \",\";\n  }\n  return data\n}\n\nvar klass$1 = {\n  staticKeys: ['staticClass'],\n  transformNode: transformNode,\n  genData: genData\n};\n\n/*  */\n\nfunction transformNode$1 (el, options) {\n  var warn = options.warn || baseWarn;\n  var staticStyle = getAndRemoveAttr(el, 'style');\n  if (staticStyle) {\n    /* istanbul ignore if */\n    {\n      var expression = parseText(staticStyle, options.delimiters);\n      if (expression) {\n        warn(\n          \"style=\\\"\" + staticStyle + \"\\\": \" +\n          'Interpolation inside attributes has been removed. ' +\n          'Use v-bind or the colon shorthand instead. For example, ' +\n          'instead of <div style=\"{{ val }}\">, use <div :style=\"val\">.'\n        );\n      }\n    }\n    el.staticStyle = JSON.stringify(parseStyleText(staticStyle));\n  }\n\n  var styleBinding = getBindingAttr(el, 'style', false /* getStatic */);\n  if (styleBinding) {\n    el.styleBinding = styleBinding;\n  }\n}\n\nfunction genData$1 (el) {\n  var data = '';\n  if (el.staticStyle) {\n    data += \"staticStyle:\" + (el.staticStyle) + \",\";\n  }\n  if (el.styleBinding) {\n    data += \"style:(\" + (el.styleBinding) + \"),\";\n  }\n  return data\n}\n\nvar style$1 = {\n  staticKeys: ['staticStyle'],\n  transformNode: transformNode$1,\n  genData: genData$1\n};\n\n/*  */\n\nvar decoder;\n\nvar he = {\n  decode: function decode (html) {\n    decoder = decoder || document.createElement('div');\n    decoder.innerHTML = html;\n    return decoder.textContent\n  }\n};\n\n/*  */\n\nvar isUnaryTag = makeMap(\n  'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +\n  'link,meta,param,source,track,wbr'\n);\n\n// Elements that you can, intentionally, leave open\n// (and which close themselves)\nvar canBeLeftOpenTag = makeMap(\n  'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source'\n);\n\n// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3\n// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content\nvar isNonPhrasingTag = makeMap(\n  'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +\n  'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +\n  'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +\n  'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +\n  'title,tr,track'\n);\n\n/**\n * Not type-checking this file because it's mostly vendor code.\n */\n\n/*!\n * HTML Parser By John Resig (ejohn.org)\n * Modified by Juriy \"kangax\" Zaytsev\n * Original code by Erik Arvidsson, Mozilla Public License\n * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js\n */\n\n// Regular Expressions for parsing tags and attributes\nvar attribute = /^\\s*([^\\s\"'<>\\/=]+)(?:\\s*(=)\\s*(?:\"([^\"]*)\"+|'([^']*)'+|([^\\s\"'=<>`]+)))?/;\n// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName\n// but for Vue templates we can enforce a simple charset\nvar ncname = '[a-zA-Z_][\\\\w\\\\-\\\\.]*';\nvar qnameCapture = \"((?:\" + ncname + \"\\\\:)?\" + ncname + \")\";\nvar startTagOpen = new RegExp((\"^<\" + qnameCapture));\nvar startTagClose = /^\\s*(\\/?)>/;\nvar endTag = new RegExp((\"^<\\\\/\" + qnameCapture + \"[^>]*>\"));\nvar doctype = /^<!DOCTYPE [^>]+>/i;\nvar comment = /^<!--/;\nvar conditionalComment = /^<!\\[/;\n\nvar IS_REGEX_CAPTURING_BROKEN = false;\n'x'.replace(/x(.)?/g, function (m, g) {\n  IS_REGEX_CAPTURING_BROKEN = g === '';\n});\n\n// Special Elements (can contain anything)\nvar isPlainTextElement = makeMap('script,style,textarea', true);\nvar reCache = {};\n\nvar decodingMap = {\n  '&lt;': '<',\n  '&gt;': '>',\n  '&quot;': '\"',\n  '&amp;': '&',\n  '&#10;': '\\n',\n  '&#9;': '\\t'\n};\nvar encodedAttr = /&(?:lt|gt|quot|amp);/g;\nvar encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g;\n\n// #5992\nvar isIgnoreNewlineTag = makeMap('pre,textarea', true);\nvar shouldIgnoreFirstNewline = function (tag, html) { return tag && isIgnoreNewlineTag(tag) && html[0] === '\\n'; };\n\nfunction decodeAttr (value, shouldDecodeNewlines) {\n  var re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;\n  return value.replace(re, function (match) { return decodingMap[match]; })\n}\n\nfunction parseHTML (html, options) {\n  var stack = [];\n  var expectHTML = options.expectHTML;\n  var isUnaryTag$$1 = options.isUnaryTag || no;\n  var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;\n  var index = 0;\n  var last, lastTag;\n  while (html) {\n    last = html;\n    // Make sure we're not in a plaintext content element like script/style\n    if (!lastTag || !isPlainTextElement(lastTag)) {\n      var textEnd = html.indexOf('<');\n      if (textEnd === 0) {\n        // Comment:\n        if (comment.test(html)) {\n          var commentEnd = html.indexOf('-->');\n\n          if (commentEnd >= 0) {\n            if (options.shouldKeepComment) {\n              options.comment(html.substring(4, commentEnd));\n            }\n            advance(commentEnd + 3);\n            continue\n          }\n        }\n\n        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment\n        if (conditionalComment.test(html)) {\n          var conditionalEnd = html.indexOf(']>');\n\n          if (conditionalEnd >= 0) {\n            advance(conditionalEnd + 2);\n            continue\n          }\n        }\n\n        // Doctype:\n        var doctypeMatch = html.match(doctype);\n        if (doctypeMatch) {\n          advance(doctypeMatch[0].length);\n          continue\n        }\n\n        // End tag:\n        var endTagMatch = html.match(endTag);\n        if (endTagMatch) {\n          var curIndex = index;\n          advance(endTagMatch[0].length);\n          parseEndTag(endTagMatch[1], curIndex, index);\n          continue\n        }\n\n        // Start tag:\n        var startTagMatch = parseStartTag();\n        if (startTagMatch) {\n          handleStartTag(startTagMatch);\n          if (shouldIgnoreFirstNewline(lastTag, html)) {\n            advance(1);\n          }\n          continue\n        }\n      }\n\n      var text = (void 0), rest = (void 0), next = (void 0);\n      if (textEnd >= 0) {\n        rest = html.slice(textEnd);\n        while (\n          !endTag.test(rest) &&\n          !startTagOpen.test(rest) &&\n          !comment.test(rest) &&\n          !conditionalComment.test(rest)\n        ) {\n          // < in plain text, be forgiving and treat it as text\n          next = rest.indexOf('<', 1);\n          if (next < 0) { break }\n          textEnd += next;\n          rest = html.slice(textEnd);\n        }\n        text = html.substring(0, textEnd);\n        advance(textEnd);\n      }\n\n      if (textEnd < 0) {\n        text = html;\n        html = '';\n      }\n\n      if (options.chars && text) {\n        options.chars(text);\n      }\n    } else {\n      var endTagLength = 0;\n      var stackedTag = lastTag.toLowerCase();\n      var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\\\s\\\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));\n      var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {\n        endTagLength = endTag.length;\n        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {\n          text = text\n            .replace(/<!--([\\s\\S]*?)-->/g, '$1')\n            .replace(/<!\\[CDATA\\[([\\s\\S]*?)]]>/g, '$1');\n        }\n        if (shouldIgnoreFirstNewline(stackedTag, text)) {\n          text = text.slice(1);\n        }\n        if (options.chars) {\n          options.chars(text);\n        }\n        return ''\n      });\n      index += html.length - rest$1.length;\n      html = rest$1;\n      parseEndTag(stackedTag, index - endTagLength, index);\n    }\n\n    if (html === last) {\n      options.chars && options.chars(html);\n      if (\"development\" !== 'production' && !stack.length && options.warn) {\n        options.warn((\"Mal-formatted tag at end of template: \\\"\" + html + \"\\\"\"));\n      }\n      break\n    }\n  }\n\n  // Clean up any remaining tags\n  parseEndTag();\n\n  function advance (n) {\n    index += n;\n    html = html.substring(n);\n  }\n\n  function parseStartTag () {\n    var start = html.match(startTagOpen);\n    if (start) {\n      var match = {\n        tagName: start[1],\n        attrs: [],\n        start: index\n      };\n      advance(start[0].length);\n      var end, attr;\n      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {\n        advance(attr[0].length);\n        match.attrs.push(attr);\n      }\n      if (end) {\n        match.unarySlash = end[1];\n        advance(end[0].length);\n        match.end = index;\n        return match\n      }\n    }\n  }\n\n  function handleStartTag (match) {\n    var tagName = match.tagName;\n    var unarySlash = match.unarySlash;\n\n    if (expectHTML) {\n      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {\n        parseEndTag(lastTag);\n      }\n      if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {\n        parseEndTag(tagName);\n      }\n    }\n\n    var unary = isUnaryTag$$1(tagName) || !!unarySlash;\n\n    var l = match.attrs.length;\n    var attrs = new Array(l);\n    for (var i = 0; i < l; i++) {\n      var args = match.attrs[i];\n      // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778\n      if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('\"\"') === -1) {\n        if (args[3] === '') { delete args[3]; }\n        if (args[4] === '') { delete args[4]; }\n        if (args[5] === '') { delete args[5]; }\n      }\n      var value = args[3] || args[4] || args[5] || '';\n      var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'\n        ? options.shouldDecodeNewlinesForHref\n        : options.shouldDecodeNewlines;\n      attrs[i] = {\n        name: args[1],\n        value: decodeAttr(value, shouldDecodeNewlines)\n      };\n    }\n\n    if (!unary) {\n      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs });\n      lastTag = tagName;\n    }\n\n    if (options.start) {\n      options.start(tagName, attrs, unary, match.start, match.end);\n    }\n  }\n\n  function parseEndTag (tagName, start, end) {\n    var pos, lowerCasedTagName;\n    if (start == null) { start = index; }\n    if (end == null) { end = index; }\n\n    if (tagName) {\n      lowerCasedTagName = tagName.toLowerCase();\n    }\n\n    // Find the closest opened tag of the same type\n    if (tagName) {\n      for (pos = stack.length - 1; pos >= 0; pos--) {\n        if (stack[pos].lowerCasedTag === lowerCasedTagName) {\n          break\n        }\n      }\n    } else {\n      // If no tag name is provided, clean shop\n      pos = 0;\n    }\n\n    if (pos >= 0) {\n      // Close all the open elements, up the stack\n      for (var i = stack.length - 1; i >= pos; i--) {\n        if (\"development\" !== 'production' &&\n          (i > pos || !tagName) &&\n          options.warn\n        ) {\n          options.warn(\n            (\"tag <\" + (stack[i].tag) + \"> has no matching end tag.\")\n          );\n        }\n        if (options.end) {\n          options.end(stack[i].tag, start, end);\n        }\n      }\n\n      // Remove the open elements from the stack\n      stack.length = pos;\n      lastTag = pos && stack[pos - 1].tag;\n    } else if (lowerCasedTagName === 'br') {\n      if (options.start) {\n        options.start(tagName, [], true, start, end);\n      }\n    } else if (lowerCasedTagName === 'p') {\n      if (options.start) {\n        options.start(tagName, [], false, start, end);\n      }\n      if (options.end) {\n        options.end(tagName, start, end);\n      }\n    }\n  }\n}\n\n/*  */\n\nvar onRE = /^@|^v-on:/;\nvar dirRE = /^v-|^@|^:/;\nvar forAliasRE = /(.*?)\\s+(?:in|of)\\s+(.*)/;\nvar forIteratorRE = /,([^,\\}\\]]*)(?:,([^,\\}\\]]*))?$/;\nvar stripParensRE = /^\\(|\\)$/g;\n\nvar argRE = /:(.*)$/;\nvar bindRE = /^:|^v-bind:/;\nvar modifierRE = /\\.[^.]+/g;\n\nvar decodeHTMLCached = cached(he.decode);\n\n// configurable state\nvar warn$2;\nvar delimiters;\nvar transforms;\nvar preTransforms;\nvar postTransforms;\nvar platformIsPreTag;\nvar platformMustUseProp;\nvar platformGetTagNamespace;\n\n\n\nfunction createASTElement (\n  tag,\n  attrs,\n  parent\n) {\n  return {\n    type: 1,\n    tag: tag,\n    attrsList: attrs,\n    attrsMap: makeAttrsMap(attrs),\n    parent: parent,\n    children: []\n  }\n}\n\n/**\n * Convert HTML string to AST.\n */\nfunction parse (\n  template,\n  options\n) {\n  warn$2 = options.warn || baseWarn;\n\n  platformIsPreTag = options.isPreTag || no;\n  platformMustUseProp = options.mustUseProp || no;\n  platformGetTagNamespace = options.getTagNamespace || no;\n\n  transforms = pluckModuleFunction(options.modules, 'transformNode');\n  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');\n  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');\n\n  delimiters = options.delimiters;\n\n  var stack = [];\n  var preserveWhitespace = options.preserveWhitespace !== false;\n  var root;\n  var currentParent;\n  var inVPre = false;\n  var inPre = false;\n  var warned = false;\n\n  function warnOnce (msg) {\n    if (!warned) {\n      warned = true;\n      warn$2(msg);\n    }\n  }\n\n  function endPre (element) {\n    // check pre state\n    if (element.pre) {\n      inVPre = false;\n    }\n    if (platformIsPreTag(element.tag)) {\n      inPre = false;\n    }\n  }\n\n  parseHTML(template, {\n    warn: warn$2,\n    expectHTML: options.expectHTML,\n    isUnaryTag: options.isUnaryTag,\n    canBeLeftOpenTag: options.canBeLeftOpenTag,\n    shouldDecodeNewlines: options.shouldDecodeNewlines,\n    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,\n    shouldKeepComment: options.comments,\n    start: function start (tag, attrs, unary) {\n      // check namespace.\n      // inherit parent ns if there is one\n      var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);\n\n      // handle IE svg bug\n      /* istanbul ignore if */\n      if (isIE && ns === 'svg') {\n        attrs = guardIESVGBug(attrs);\n      }\n\n      var element = createASTElement(tag, attrs, currentParent);\n      if (ns) {\n        element.ns = ns;\n      }\n\n      if (isForbiddenTag(element) && !isServerRendering()) {\n        element.forbidden = true;\n        \"development\" !== 'production' && warn$2(\n          'Templates should only be responsible for mapping the state to the ' +\n          'UI. Avoid placing tags with side-effects in your templates, such as ' +\n          \"<\" + tag + \">\" + ', as they will not be parsed.'\n        );\n      }\n\n      // apply pre-transforms\n      for (var i = 0; i < preTransforms.length; i++) {\n        element = preTransforms[i](element, options) || element;\n      }\n\n      if (!inVPre) {\n        processPre(element);\n        if (element.pre) {\n          inVPre = true;\n        }\n      }\n      if (platformIsPreTag(element.tag)) {\n        inPre = true;\n      }\n      if (inVPre) {\n        processRawAttrs(element);\n      } else if (!element.processed) {\n        // structural directives\n        processFor(element);\n        processIf(element);\n        processOnce(element);\n        // element-scope stuff\n        processElement(element, options);\n      }\n\n      function checkRootConstraints (el) {\n        {\n          if (el.tag === 'slot' || el.tag === 'template') {\n            warnOnce(\n              \"Cannot use <\" + (el.tag) + \"> as component root element because it may \" +\n              'contain multiple nodes.'\n            );\n          }\n          if (el.attrsMap.hasOwnProperty('v-for')) {\n            warnOnce(\n              'Cannot use v-for on stateful component root element because ' +\n              'it renders multiple elements.'\n            );\n          }\n        }\n      }\n\n      // tree management\n      if (!root) {\n        root = element;\n        checkRootConstraints(root);\n      } else if (!stack.length) {\n        // allow root elements with v-if, v-else-if and v-else\n        if (root.if && (element.elseif || element.else)) {\n          checkRootConstraints(element);\n          addIfCondition(root, {\n            exp: element.elseif,\n            block: element\n          });\n        } else {\n          warnOnce(\n            \"Component template should contain exactly one root element. \" +\n            \"If you are using v-if on multiple elements, \" +\n            \"use v-else-if to chain them instead.\"\n          );\n        }\n      }\n      if (currentParent && !element.forbidden) {\n        if (element.elseif || element.else) {\n          processIfConditions(element, currentParent);\n        } else if (element.slotScope) { // scoped slot\n          currentParent.plain = false;\n          var name = element.slotTarget || '\"default\"';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;\n        } else {\n          currentParent.children.push(element);\n          element.parent = currentParent;\n        }\n      }\n      if (!unary) {\n        currentParent = element;\n        stack.push(element);\n      } else {\n        endPre(element);\n      }\n      // apply post-transforms\n      for (var i$1 = 0; i$1 < postTransforms.length; i$1++) {\n        postTransforms[i$1](element, options);\n      }\n    },\n\n    end: function end () {\n      // remove trailing whitespace\n      var element = stack[stack.length - 1];\n      var lastNode = element.children[element.children.length - 1];\n      if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {\n        element.children.pop();\n      }\n      // pop stack\n      stack.length -= 1;\n      currentParent = stack[stack.length - 1];\n      endPre(element);\n    },\n\n    chars: function chars (text) {\n      if (!currentParent) {\n        {\n          if (text === template) {\n            warnOnce(\n              'Component template requires a root element, rather than just text.'\n            );\n          } else if ((text = text.trim())) {\n            warnOnce(\n              (\"text \\\"\" + text + \"\\\" outside root element will be ignored.\")\n            );\n          }\n        }\n        return\n      }\n      // IE textarea placeholder bug\n      /* istanbul ignore if */\n      if (isIE &&\n        currentParent.tag === 'textarea' &&\n        currentParent.attrsMap.placeholder === text\n      ) {\n        return\n      }\n      var children = currentParent.children;\n      text = inPre || text.trim()\n        ? isTextTag(currentParent) ? text : decodeHTMLCached(text)\n        // only preserve whitespace if its not right after a starting tag\n        : preserveWhitespace && children.length ? ' ' : '';\n      if (text) {\n        var expression;\n        if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {\n          children.push({\n            type: 2,\n            expression: expression,\n            text: text\n          });\n        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {\n          children.push({\n            type: 3,\n            text: text\n          });\n        }\n      }\n    },\n    comment: function comment (text) {\n      currentParent.children.push({\n        type: 3,\n        text: text,\n        isComment: true\n      });\n    }\n  });\n  return root\n}\n\nfunction processPre (el) {\n  if (getAndRemoveAttr(el, 'v-pre') != null) {\n    el.pre = true;\n  }\n}\n\nfunction processRawAttrs (el) {\n  var l = el.attrsList.length;\n  if (l) {\n    var attrs = el.attrs = new Array(l);\n    for (var i = 0; i < l; i++) {\n      attrs[i] = {\n        name: el.attrsList[i].name,\n        value: JSON.stringify(el.attrsList[i].value)\n      };\n    }\n  } else if (!el.pre) {\n    // non root node in pre blocks with no attributes\n    el.plain = true;\n  }\n}\n\nfunction processElement (element, options) {\n  processKey(element);\n\n  // determine whether this is a plain element after\n  // removing structural attributes\n  element.plain = !element.key && !element.attrsList.length;\n\n  processRef(element);\n  processSlot(element);\n  processComponent(element);\n  for (var i = 0; i < transforms.length; i++) {\n    element = transforms[i](element, options) || element;\n  }\n  processAttrs(element);\n}\n\nfunction processKey (el) {\n  var exp = getBindingAttr(el, 'key');\n  if (exp) {\n    if (\"development\" !== 'production' && el.tag === 'template') {\n      warn$2(\"<template> cannot be keyed. Place the key on real elements instead.\");\n    }\n    el.key = exp;\n  }\n}\n\nfunction processRef (el) {\n  var ref = getBindingAttr(el, 'ref');\n  if (ref) {\n    el.ref = ref;\n    el.refInFor = checkInFor(el);\n  }\n}\n\nfunction processFor (el) {\n  var exp;\n  if ((exp = getAndRemoveAttr(el, 'v-for'))) {\n    var inMatch = exp.match(forAliasRE);\n    if (!inMatch) {\n      \"development\" !== 'production' && warn$2(\n        (\"Invalid v-for expression: \" + exp)\n      );\n      return\n    }\n    el.for = inMatch[2].trim();\n    var alias = inMatch[1].trim().replace(stripParensRE, '');\n    var iteratorMatch = alias.match(forIteratorRE);\n    if (iteratorMatch) {\n      el.alias = alias.replace(forIteratorRE, '');\n      el.iterator1 = iteratorMatch[1].trim();\n      if (iteratorMatch[2]) {\n        el.iterator2 = iteratorMatch[2].trim();\n      }\n    } else {\n      el.alias = alias;\n    }\n  }\n}\n\nfunction processIf (el) {\n  var exp = getAndRemoveAttr(el, 'v-if');\n  if (exp) {\n    el.if = exp;\n    addIfCondition(el, {\n      exp: exp,\n      block: el\n    });\n  } else {\n    if (getAndRemoveAttr(el, 'v-else') != null) {\n      el.else = true;\n    }\n    var elseif = getAndRemoveAttr(el, 'v-else-if');\n    if (elseif) {\n      el.elseif = elseif;\n    }\n  }\n}\n\nfunction processIfConditions (el, parent) {\n  var prev = findPrevElement(parent.children);\n  if (prev && prev.if) {\n    addIfCondition(prev, {\n      exp: el.elseif,\n      block: el\n    });\n  } else {\n    warn$2(\n      \"v-\" + (el.elseif ? ('else-if=\"' + el.elseif + '\"') : 'else') + \" \" +\n      \"used on element <\" + (el.tag) + \"> without corresponding v-if.\"\n    );\n  }\n}\n\nfunction findPrevElement (children) {\n  var i = children.length;\n  while (i--) {\n    if (children[i].type === 1) {\n      return children[i]\n    } else {\n      if (\"development\" !== 'production' && children[i].text !== ' ') {\n        warn$2(\n          \"text \\\"\" + (children[i].text.trim()) + \"\\\" between v-if and v-else(-if) \" +\n          \"will be ignored.\"\n        );\n      }\n      children.pop();\n    }\n  }\n}\n\nfunction addIfCondition (el, condition) {\n  if (!el.ifConditions) {\n    el.ifConditions = [];\n  }\n  el.ifConditions.push(condition);\n}\n\nfunction processOnce (el) {\n  var once$$1 = getAndRemoveAttr(el, 'v-once');\n  if (once$$1 != null) {\n    el.once = true;\n  }\n}\n\nfunction processSlot (el) {\n  if (el.tag === 'slot') {\n    el.slotName = getBindingAttr(el, 'name');\n    if (\"development\" !== 'production' && el.key) {\n      warn$2(\n        \"`key` does not work on <slot> because slots are abstract outlets \" +\n        \"and can possibly expand into multiple elements. \" +\n        \"Use the key on a wrapping element instead.\"\n      );\n    }\n  } else {\n    var slotScope;\n    if (el.tag === 'template') {\n      slotScope = getAndRemoveAttr(el, 'scope');\n      /* istanbul ignore if */\n      if (\"development\" !== 'production' && slotScope) {\n        warn$2(\n          \"the \\\"scope\\\" attribute for scoped slots have been deprecated and \" +\n          \"replaced by \\\"slot-scope\\\" since 2.5. The new \\\"slot-scope\\\" attribute \" +\n          \"can also be used on plain elements in addition to <template> to \" +\n          \"denote scoped slots.\",\n          true\n        );\n      }\n      el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');\n    } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {\n      /* istanbul ignore if */\n      if (\"development\" !== 'production' && el.attrsMap['v-for']) {\n        warn$2(\n          \"Ambiguous combined usage of slot-scope and v-for on <\" + (el.tag) + \"> \" +\n          \"(v-for takes higher priority). Use a wrapper <template> for the \" +\n          \"scoped slot to make it clearer.\",\n          true\n        );\n      }\n      el.slotScope = slotScope;\n    }\n    var slotTarget = getBindingAttr(el, 'slot');\n    if (slotTarget) {\n      el.slotTarget = slotTarget === '\"\"' ? '\"default\"' : slotTarget;\n      // preserve slot as an attribute for native shadow DOM compat\n      // only for non-scoped slots.\n      if (el.tag !== 'template' && !el.slotScope) {\n        addAttr(el, 'slot', slotTarget);\n      }\n    }\n  }\n}\n\nfunction processComponent (el) {\n  var binding;\n  if ((binding = getBindingAttr(el, 'is'))) {\n    el.component = binding;\n  }\n  if (getAndRemoveAttr(el, 'inline-template') != null) {\n    el.inlineTemplate = true;\n  }\n}\n\nfunction processAttrs (el) {\n  var list = el.attrsList;\n  var i, l, name, rawName, value, modifiers, isProp;\n  for (i = 0, l = list.length; i < l; i++) {\n    name = rawName = list[i].name;\n    value = list[i].value;\n    if (dirRE.test(name)) {\n      // mark element as dynamic\n      el.hasBindings = true;\n      // modifiers\n      modifiers = parseModifiers(name);\n      if (modifiers) {\n        name = name.replace(modifierRE, '');\n      }\n      if (bindRE.test(name)) { // v-bind\n        name = name.replace(bindRE, '');\n        value = parseFilters(value);\n        isProp = false;\n        if (modifiers) {\n          if (modifiers.prop) {\n            isProp = true;\n            name = camelize(name);\n            if (name === 'innerHtml') { name = 'innerHTML'; }\n          }\n          if (modifiers.camel) {\n            name = camelize(name);\n          }\n          if (modifiers.sync) {\n            addHandler(\n              el,\n              (\"update:\" + (camelize(name))),\n              genAssignmentCode(value, \"$event\")\n            );\n          }\n        }\n        if (isProp || (\n          !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)\n        )) {\n          addProp(el, name, value);\n        } else {\n          addAttr(el, name, value);\n        }\n      } else if (onRE.test(name)) { // v-on\n        name = name.replace(onRE, '');\n        addHandler(el, name, value, modifiers, false, warn$2);\n      } else { // normal directives\n        name = name.replace(dirRE, '');\n        // parse arg\n        var argMatch = name.match(argRE);\n        var arg = argMatch && argMatch[1];\n        if (arg) {\n          name = name.slice(0, -(arg.length + 1));\n        }\n        addDirective(el, name, rawName, value, arg, modifiers);\n        if (\"development\" !== 'production' && name === 'model') {\n          checkForAliasModel(el, value);\n        }\n      }\n    } else {\n      // literal attribute\n      {\n        var expression = parseText(value, delimiters);\n        if (expression) {\n          warn$2(\n            name + \"=\\\"\" + value + \"\\\": \" +\n            'Interpolation inside attributes has been removed. ' +\n            'Use v-bind or the colon shorthand instead. For example, ' +\n            'instead of <div id=\"{{ val }}\">, use <div :id=\"val\">.'\n          );\n        }\n      }\n      addAttr(el, name, JSON.stringify(value));\n      // #6887 firefox doesn't update muted state if set via attribute\n      // even immediately after element creation\n      if (!el.component &&\n          name === 'muted' &&\n          platformMustUseProp(el.tag, el.attrsMap.type, name)) {\n        addProp(el, name, 'true');\n      }\n    }\n  }\n}\n\nfunction checkInFor (el) {\n  var parent = el;\n  while (parent) {\n    if (parent.for !== undefined) {\n      return true\n    }\n    parent = parent.parent;\n  }\n  return false\n}\n\nfunction parseModifiers (name) {\n  var match = name.match(modifierRE);\n  if (match) {\n    var ret = {};\n    match.forEach(function (m) { ret[m.slice(1)] = true; });\n    return ret\n  }\n}\n\nfunction makeAttrsMap (attrs) {\n  var map = {};\n  for (var i = 0, l = attrs.length; i < l; i++) {\n    if (\n      \"development\" !== 'production' &&\n      map[attrs[i].name] && !isIE && !isEdge\n    ) {\n      warn$2('duplicate attribute: ' + attrs[i].name);\n    }\n    map[attrs[i].name] = attrs[i].value;\n  }\n  return map\n}\n\n// for script (e.g. type=\"x/template\") or style, do not decode content\nfunction isTextTag (el) {\n  return el.tag === 'script' || el.tag === 'style'\n}\n\nfunction isForbiddenTag (el) {\n  return (\n    el.tag === 'style' ||\n    (el.tag === 'script' && (\n      !el.attrsMap.type ||\n      el.attrsMap.type === 'text/javascript'\n    ))\n  )\n}\n\nvar ieNSBug = /^xmlns:NS\\d+/;\nvar ieNSPrefix = /^NS\\d+:/;\n\n/* istanbul ignore next */\nfunction guardIESVGBug (attrs) {\n  var res = [];\n  for (var i = 0; i < attrs.length; i++) {\n    var attr = attrs[i];\n    if (!ieNSBug.test(attr.name)) {\n      attr.name = attr.name.replace(ieNSPrefix, '');\n      res.push(attr);\n    }\n  }\n  return res\n}\n\nfunction checkForAliasModel (el, value) {\n  var _el = el;\n  while (_el) {\n    if (_el.for && _el.alias === value) {\n      warn$2(\n        \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\">: \" +\n        \"You are binding v-model directly to a v-for iteration alias. \" +\n        \"This will not be able to modify the v-for source array because \" +\n        \"writing to the alias is like modifying a function local variable. \" +\n        \"Consider using an array of objects and use v-model on an object property instead.\"\n      );\n    }\n    _el = _el.parent;\n  }\n}\n\n/*  */\n\n/**\n * Expand input[v-model] with dyanmic type bindings into v-if-else chains\n * Turn this:\n *   <input v-model=\"data[type]\" :type=\"type\">\n * into this:\n *   <input v-if=\"type === 'checkbox'\" type=\"checkbox\" v-model=\"data[type]\">\n *   <input v-else-if=\"type === 'radio'\" type=\"radio\" v-model=\"data[type]\">\n *   <input v-else :type=\"type\" v-model=\"data[type]\">\n */\n\nfunction preTransformNode (el, options) {\n  if (el.tag === 'input') {\n    var map = el.attrsMap;\n    if (map['v-model'] && (map['v-bind:type'] || map[':type'])) {\n      var typeBinding = getBindingAttr(el, 'type');\n      var ifCondition = getAndRemoveAttr(el, 'v-if', true);\n      var ifConditionExtra = ifCondition ? (\"&&(\" + ifCondition + \")\") : \"\";\n      var hasElse = getAndRemoveAttr(el, 'v-else', true) != null;\n      var elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true);\n      // 1. checkbox\n      var branch0 = cloneASTElement(el);\n      // process for on the main node\n      processFor(branch0);\n      addRawAttr(branch0, 'type', 'checkbox');\n      processElement(branch0, options);\n      branch0.processed = true; // prevent it from double-processed\n      branch0.if = \"(\" + typeBinding + \")==='checkbox'\" + ifConditionExtra;\n      addIfCondition(branch0, {\n        exp: branch0.if,\n        block: branch0\n      });\n      // 2. add radio else-if condition\n      var branch1 = cloneASTElement(el);\n      getAndRemoveAttr(branch1, 'v-for', true);\n      addRawAttr(branch1, 'type', 'radio');\n      processElement(branch1, options);\n      addIfCondition(branch0, {\n        exp: \"(\" + typeBinding + \")==='radio'\" + ifConditionExtra,\n        block: branch1\n      });\n      // 3. other\n      var branch2 = cloneASTElement(el);\n      getAndRemoveAttr(branch2, 'v-for', true);\n      addRawAttr(branch2, ':type', typeBinding);\n      processElement(branch2, options);\n      addIfCondition(branch0, {\n        exp: ifCondition,\n        block: branch2\n      });\n\n      if (hasElse) {\n        branch0.else = true;\n      } else if (elseIfCondition) {\n        branch0.elseif = elseIfCondition;\n      }\n\n      return branch0\n    }\n  }\n}\n\nfunction cloneASTElement (el) {\n  return createASTElement(el.tag, el.attrsList.slice(), el.parent)\n}\n\nfunction addRawAttr (el, name, value) {\n  el.attrsMap[name] = value;\n  el.attrsList.push({ name: name, value: value });\n}\n\nvar model$2 = {\n  preTransformNode: preTransformNode\n};\n\nvar modules$1 = [\n  klass$1,\n  style$1,\n  model$2\n];\n\n/*  */\n\nfunction text (el, dir) {\n  if (dir.value) {\n    addProp(el, 'textContent', (\"_s(\" + (dir.value) + \")\"));\n  }\n}\n\n/*  */\n\nfunction html (el, dir) {\n  if (dir.value) {\n    addProp(el, 'innerHTML', (\"_s(\" + (dir.value) + \")\"));\n  }\n}\n\nvar directives$1 = {\n  model: model,\n  text: text,\n  html: html\n};\n\n/*  */\n\nvar baseOptions = {\n  expectHTML: true,\n  modules: modules$1,\n  directives: directives$1,\n  isPreTag: isPreTag,\n  isUnaryTag: isUnaryTag,\n  mustUseProp: mustUseProp,\n  canBeLeftOpenTag: canBeLeftOpenTag,\n  isReservedTag: isReservedTag,\n  getTagNamespace: getTagNamespace,\n  staticKeys: genStaticKeys(modules$1)\n};\n\n/*  */\n\nvar isStaticKey;\nvar isPlatformReservedTag;\n\nvar genStaticKeysCached = cached(genStaticKeys$1);\n\n/**\n * Goal of the optimizer: walk the generated template AST tree\n * and detect sub-trees that are purely static, i.e. parts of\n * the DOM that never needs to change.\n *\n * Once we detect these sub-trees, we can:\n *\n * 1. Hoist them into constants, so that we no longer need to\n *    create fresh nodes for them on each re-render;\n * 2. Completely skip them in the patching process.\n */\nfunction optimize (root, options) {\n  if (!root) { return }\n  isStaticKey = genStaticKeysCached(options.staticKeys || '');\n  isPlatformReservedTag = options.isReservedTag || no;\n  // first pass: mark all non-static nodes.\n  markStatic$1(root);\n  // second pass: mark static roots.\n  markStaticRoots(root, false);\n}\n\nfunction genStaticKeys$1 (keys) {\n  return makeMap(\n    'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +\n    (keys ? ',' + keys : '')\n  )\n}\n\nfunction markStatic$1 (node) {\n  node.static = isStatic(node);\n  if (node.type === 1) {\n    // do not make component slot content static. this avoids\n    // 1. components not able to mutate slot nodes\n    // 2. static slot content fails for hot-reloading\n    if (\n      !isPlatformReservedTag(node.tag) &&\n      node.tag !== 'slot' &&\n      node.attrsMap['inline-template'] == null\n    ) {\n      return\n    }\n    for (var i = 0, l = node.children.length; i < l; i++) {\n      var child = node.children[i];\n      markStatic$1(child);\n      if (!child.static) {\n        node.static = false;\n      }\n    }\n    if (node.ifConditions) {\n      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {\n        var block = node.ifConditions[i$1].block;\n        markStatic$1(block);\n        if (!block.static) {\n          node.static = false;\n        }\n      }\n    }\n  }\n}\n\nfunction markStaticRoots (node, isInFor) {\n  if (node.type === 1) {\n    if (node.static || node.once) {\n      node.staticInFor = isInFor;\n    }\n    // For a node to qualify as a static root, it should have children that\n    // are not just static text. Otherwise the cost of hoisting out will\n    // outweigh the benefits and it's better off to just always render it fresh.\n    if (node.static && node.children.length && !(\n      node.children.length === 1 &&\n      node.children[0].type === 3\n    )) {\n      node.staticRoot = true;\n      return\n    } else {\n      node.staticRoot = false;\n    }\n    if (node.children) {\n      for (var i = 0, l = node.children.length; i < l; i++) {\n        markStaticRoots(node.children[i], isInFor || !!node.for);\n      }\n    }\n    if (node.ifConditions) {\n      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {\n        markStaticRoots(node.ifConditions[i$1].block, isInFor);\n      }\n    }\n  }\n}\n\nfunction isStatic (node) {\n  if (node.type === 2) { // expression\n    return false\n  }\n  if (node.type === 3) { // text\n    return true\n  }\n  return !!(node.pre || (\n    !node.hasBindings && // no dynamic bindings\n    !node.if && !node.for && // not v-if or v-for or v-else\n    !isBuiltInTag(node.tag) && // not a built-in\n    isPlatformReservedTag(node.tag) && // not a component\n    !isDirectChildOfTemplateFor(node) &&\n    Object.keys(node).every(isStaticKey)\n  ))\n}\n\nfunction isDirectChildOfTemplateFor (node) {\n  while (node.parent) {\n    node = node.parent;\n    if (node.tag !== 'template') {\n      return false\n    }\n    if (node.for) {\n      return true\n    }\n  }\n  return false\n}\n\n/*  */\n\nvar fnExpRE = /^\\s*([\\w$_]+|\\([^)]*?\\))\\s*=>|^function\\s*\\(/;\nvar simplePathRE = /^\\s*[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*|\\['.*?']|\\[\".*?\"]|\\[\\d+]|\\[[A-Za-z_$][\\w$]*])*\\s*$/;\n\n// keyCode aliases\nvar keyCodes = {\n  esc: 27,\n  tab: 9,\n  enter: 13,\n  space: 32,\n  up: 38,\n  left: 37,\n  right: 39,\n  down: 40,\n  'delete': [8, 46]\n};\n\n// #4868: modifiers that prevent the execution of the listener\n// need to explicitly return null so that we can determine whether to remove\n// the listener for .once\nvar genGuard = function (condition) { return (\"if(\" + condition + \")return null;\"); };\n\nvar modifierCode = {\n  stop: '$event.stopPropagation();',\n  prevent: '$event.preventDefault();',\n  self: genGuard(\"$event.target !== $event.currentTarget\"),\n  ctrl: genGuard(\"!$event.ctrlKey\"),\n  shift: genGuard(\"!$event.shiftKey\"),\n  alt: genGuard(\"!$event.altKey\"),\n  meta: genGuard(\"!$event.metaKey\"),\n  left: genGuard(\"'button' in $event && $event.button !== 0\"),\n  middle: genGuard(\"'button' in $event && $event.button !== 1\"),\n  right: genGuard(\"'button' in $event && $event.button !== 2\")\n};\n\nfunction genHandlers (\n  events,\n  isNative,\n  warn\n) {\n  var res = isNative ? 'nativeOn:{' : 'on:{';\n  for (var name in events) {\n    res += \"\\\"\" + name + \"\\\":\" + (genHandler(name, events[name])) + \",\";\n  }\n  return res.slice(0, -1) + '}'\n}\n\nfunction genHandler (\n  name,\n  handler\n) {\n  if (!handler) {\n    return 'function(){}'\n  }\n\n  if (Array.isArray(handler)) {\n    return (\"[\" + (handler.map(function (handler) { return genHandler(name, handler); }).join(',')) + \"]\")\n  }\n\n  var isMethodPath = simplePathRE.test(handler.value);\n  var isFunctionExpression = fnExpRE.test(handler.value);\n\n  if (!handler.modifiers) {\n    return isMethodPath || isFunctionExpression\n      ? handler.value\n      : (\"function($event){\" + (handler.value) + \"}\") // inline statement\n  } else {\n    var code = '';\n    var genModifierCode = '';\n    var keys = [];\n    for (var key in handler.modifiers) {\n      if (modifierCode[key]) {\n        genModifierCode += modifierCode[key];\n        // left/right\n        if (keyCodes[key]) {\n          keys.push(key);\n        }\n      } else if (key === 'exact') {\n        var modifiers = (handler.modifiers);\n        genModifierCode += genGuard(\n          ['ctrl', 'shift', 'alt', 'meta']\n            .filter(function (keyModifier) { return !modifiers[keyModifier]; })\n            .map(function (keyModifier) { return (\"$event.\" + keyModifier + \"Key\"); })\n            .join('||')\n        );\n      } else {\n        keys.push(key);\n      }\n    }\n    if (keys.length) {\n      code += genKeyFilter(keys);\n    }\n    // Make sure modifiers like prevent and stop get executed after key filtering\n    if (genModifierCode) {\n      code += genModifierCode;\n    }\n    var handlerCode = isMethodPath\n      ? handler.value + '($event)'\n      : isFunctionExpression\n        ? (\"(\" + (handler.value) + \")($event)\")\n        : handler.value;\n    return (\"function($event){\" + code + handlerCode + \"}\")\n  }\n}\n\nfunction genKeyFilter (keys) {\n  return (\"if(!('button' in $event)&&\" + (keys.map(genFilterCode).join('&&')) + \")return null;\")\n}\n\nfunction genFilterCode (key) {\n  var keyVal = parseInt(key, 10);\n  if (keyVal) {\n    return (\"$event.keyCode!==\" + keyVal)\n  }\n  var code = keyCodes[key];\n  return (\n    \"_k($event.keyCode,\" +\n    (JSON.stringify(key)) + \",\" +\n    (JSON.stringify(code)) + \",\" +\n    \"$event.key)\"\n  )\n}\n\n/*  */\n\nfunction on (el, dir) {\n  if (\"development\" !== 'production' && dir.modifiers) {\n    warn(\"v-on without argument does not support modifiers.\");\n  }\n  el.wrapListeners = function (code) { return (\"_g(\" + code + \",\" + (dir.value) + \")\"); };\n}\n\n/*  */\n\nfunction bind$1 (el, dir) {\n  el.wrapData = function (code) {\n    return (\"_b(\" + code + \",'\" + (el.tag) + \"',\" + (dir.value) + \",\" + (dir.modifiers && dir.modifiers.prop ? 'true' : 'false') + (dir.modifiers && dir.modifiers.sync ? ',true' : '') + \")\")\n  };\n}\n\n/*  */\n\nvar baseDirectives = {\n  on: on,\n  bind: bind$1,\n  cloak: noop\n};\n\n/*  */\n\nvar CodegenState = function CodegenState (options) {\n  this.options = options;\n  this.warn = options.warn || baseWarn;\n  this.transforms = pluckModuleFunction(options.modules, 'transformCode');\n  this.dataGenFns = pluckModuleFunction(options.modules, 'genData');\n  this.directives = extend(extend({}, baseDirectives), options.directives);\n  var isReservedTag = options.isReservedTag || no;\n  this.maybeComponent = function (el) { return !isReservedTag(el.tag); };\n  this.onceId = 0;\n  this.staticRenderFns = [];\n};\n\n\n\nfunction generate (\n  ast,\n  options\n) {\n  var state = new CodegenState(options);\n  var code = ast ? genElement(ast, state) : '_c(\"div\")';\n  return {\n    render: (\"with(this){return \" + code + \"}\"),\n    staticRenderFns: state.staticRenderFns\n  }\n}\n\nfunction genElement (el, state) {\n  if (el.staticRoot && !el.staticProcessed) {\n    return genStatic(el, state)\n  } else if (el.once && !el.onceProcessed) {\n    return genOnce(el, state)\n  } else if (el.for && !el.forProcessed) {\n    return genFor(el, state)\n  } else if (el.if && !el.ifProcessed) {\n    return genIf(el, state)\n  } else if (el.tag === 'template' && !el.slotTarget) {\n    return genChildren(el, state) || 'void 0'\n  } else if (el.tag === 'slot') {\n    return genSlot(el, state)\n  } else {\n    // component or element\n    var code;\n    if (el.component) {\n      code = genComponent(el.component, el, state);\n    } else {\n      var data = el.plain ? undefined : genData$2(el, state);\n\n      var children = el.inlineTemplate ? null : genChildren(el, state, true);\n      code = \"_c('\" + (el.tag) + \"'\" + (data ? (\",\" + data) : '') + (children ? (\",\" + children) : '') + \")\";\n    }\n    // module transforms\n    for (var i = 0; i < state.transforms.length; i++) {\n      code = state.transforms[i](el, code);\n    }\n    return code\n  }\n}\n\n// hoist static sub-trees out\nfunction genStatic (el, state) {\n  el.staticProcessed = true;\n  state.staticRenderFns.push((\"with(this){return \" + (genElement(el, state)) + \"}\"));\n  return (\"_m(\" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + \")\")\n}\n\n// v-once\nfunction genOnce (el, state) {\n  el.onceProcessed = true;\n  if (el.if && !el.ifProcessed) {\n    return genIf(el, state)\n  } else if (el.staticInFor) {\n    var key = '';\n    var parent = el.parent;\n    while (parent) {\n      if (parent.for) {\n        key = parent.key;\n        break\n      }\n      parent = parent.parent;\n    }\n    if (!key) {\n      \"development\" !== 'production' && state.warn(\n        \"v-once can only be used inside v-for that is keyed. \"\n      );\n      return genElement(el, state)\n    }\n    return (\"_o(\" + (genElement(el, state)) + \",\" + (state.onceId++) + \",\" + key + \")\")\n  } else {\n    return genStatic(el, state)\n  }\n}\n\nfunction genIf (\n  el,\n  state,\n  altGen,\n  altEmpty\n) {\n  el.ifProcessed = true; // avoid recursion\n  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)\n}\n\nfunction genIfConditions (\n  conditions,\n  state,\n  altGen,\n  altEmpty\n) {\n  if (!conditions.length) {\n    return altEmpty || '_e()'\n  }\n\n  var condition = conditions.shift();\n  if (condition.exp) {\n    return (\"(\" + (condition.exp) + \")?\" + (genTernaryExp(condition.block)) + \":\" + (genIfConditions(conditions, state, altGen, altEmpty)))\n  } else {\n    return (\"\" + (genTernaryExp(condition.block)))\n  }\n\n  // v-if with v-once should generate code like (a)?_m(0):_m(1)\n  function genTernaryExp (el) {\n    return altGen\n      ? altGen(el, state)\n      : el.once\n        ? genOnce(el, state)\n        : genElement(el, state)\n  }\n}\n\nfunction genFor (\n  el,\n  state,\n  altGen,\n  altHelper\n) {\n  var exp = el.for;\n  var alias = el.alias;\n  var iterator1 = el.iterator1 ? (\",\" + (el.iterator1)) : '';\n  var iterator2 = el.iterator2 ? (\",\" + (el.iterator2)) : '';\n\n  if (\"development\" !== 'production' &&\n    state.maybeComponent(el) &&\n    el.tag !== 'slot' &&\n    el.tag !== 'template' &&\n    !el.key\n  ) {\n    state.warn(\n      \"<\" + (el.tag) + \" v-for=\\\"\" + alias + \" in \" + exp + \"\\\">: component lists rendered with \" +\n      \"v-for should have explicit keys. \" +\n      \"See https://vuejs.org/guide/list.html#key for more info.\",\n      true /* tip */\n    );\n  }\n\n  el.forProcessed = true; // avoid recursion\n  return (altHelper || '_l') + \"((\" + exp + \"),\" +\n    \"function(\" + alias + iterator1 + iterator2 + \"){\" +\n      \"return \" + ((altGen || genElement)(el, state)) +\n    '})'\n}\n\nfunction genData$2 (el, state) {\n  var data = '{';\n\n  // directives first.\n  // directives may mutate the el's other properties before they are generated.\n  var dirs = genDirectives(el, state);\n  if (dirs) { data += dirs + ','; }\n\n  // key\n  if (el.key) {\n    data += \"key:\" + (el.key) + \",\";\n  }\n  // ref\n  if (el.ref) {\n    data += \"ref:\" + (el.ref) + \",\";\n  }\n  if (el.refInFor) {\n    data += \"refInFor:true,\";\n  }\n  // pre\n  if (el.pre) {\n    data += \"pre:true,\";\n  }\n  // record original tag name for components using \"is\" attribute\n  if (el.component) {\n    data += \"tag:\\\"\" + (el.tag) + \"\\\",\";\n  }\n  // module data generation functions\n  for (var i = 0; i < state.dataGenFns.length; i++) {\n    data += state.dataGenFns[i](el);\n  }\n  // attributes\n  if (el.attrs) {\n    data += \"attrs:{\" + (genProps(el.attrs)) + \"},\";\n  }\n  // DOM props\n  if (el.props) {\n    data += \"domProps:{\" + (genProps(el.props)) + \"},\";\n  }\n  // event handlers\n  if (el.events) {\n    data += (genHandlers(el.events, false, state.warn)) + \",\";\n  }\n  if (el.nativeEvents) {\n    data += (genHandlers(el.nativeEvents, true, state.warn)) + \",\";\n  }\n  // slot target\n  // only for non-scoped slots\n  if (el.slotTarget && !el.slotScope) {\n    data += \"slot:\" + (el.slotTarget) + \",\";\n  }\n  // scoped slots\n  if (el.scopedSlots) {\n    data += (genScopedSlots(el.scopedSlots, state)) + \",\";\n  }\n  // component v-model\n  if (el.model) {\n    data += \"model:{value:\" + (el.model.value) + \",callback:\" + (el.model.callback) + \",expression:\" + (el.model.expression) + \"},\";\n  }\n  // inline-template\n  if (el.inlineTemplate) {\n    var inlineTemplate = genInlineTemplate(el, state);\n    if (inlineTemplate) {\n      data += inlineTemplate + \",\";\n    }\n  }\n  data = data.replace(/,$/, '') + '}';\n  // v-bind data wrap\n  if (el.wrapData) {\n    data = el.wrapData(data);\n  }\n  // v-on data wrap\n  if (el.wrapListeners) {\n    data = el.wrapListeners(data);\n  }\n  return data\n}\n\nfunction genDirectives (el, state) {\n  var dirs = el.directives;\n  if (!dirs) { return }\n  var res = 'directives:[';\n  var hasRuntime = false;\n  var i, l, dir, needRuntime;\n  for (i = 0, l = dirs.length; i < l; i++) {\n    dir = dirs[i];\n    needRuntime = true;\n    var gen = state.directives[dir.name];\n    if (gen) {\n      // compile-time directive that manipulates AST.\n      // returns true if it also needs a runtime counterpart.\n      needRuntime = !!gen(el, dir, state.warn);\n    }\n    if (needRuntime) {\n      hasRuntime = true;\n      res += \"{name:\\\"\" + (dir.name) + \"\\\",rawName:\\\"\" + (dir.rawName) + \"\\\"\" + (dir.value ? (\",value:(\" + (dir.value) + \"),expression:\" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (\",arg:\\\"\" + (dir.arg) + \"\\\"\") : '') + (dir.modifiers ? (\",modifiers:\" + (JSON.stringify(dir.modifiers))) : '') + \"},\";\n    }\n  }\n  if (hasRuntime) {\n    return res.slice(0, -1) + ']'\n  }\n}\n\nfunction genInlineTemplate (el, state) {\n  var ast = el.children[0];\n  if (\"development\" !== 'production' && (\n    el.children.length !== 1 || ast.type !== 1\n  )) {\n    state.warn('Inline-template components must have exactly one child element.');\n  }\n  if (ast.type === 1) {\n    var inlineRenderFns = generate(ast, state.options);\n    return (\"inlineTemplate:{render:function(){\" + (inlineRenderFns.render) + \"},staticRenderFns:[\" + (inlineRenderFns.staticRenderFns.map(function (code) { return (\"function(){\" + code + \"}\"); }).join(',')) + \"]}\")\n  }\n}\n\nfunction genScopedSlots (\n  slots,\n  state\n) {\n  return (\"scopedSlots:_u([\" + (Object.keys(slots).map(function (key) {\n      return genScopedSlot(key, slots[key], state)\n    }).join(',')) + \"])\")\n}\n\nfunction genScopedSlot (\n  key,\n  el,\n  state\n) {\n  if (el.for && !el.forProcessed) {\n    return genForScopedSlot(key, el, state)\n  }\n  var fn = \"function(\" + (String(el.slotScope)) + \"){\" +\n    \"return \" + (el.tag === 'template'\n      ? el.if\n        ? ((el.if) + \"?\" + (genChildren(el, state) || 'undefined') + \":undefined\")\n        : genChildren(el, state) || 'undefined'\n      : genElement(el, state)) + \"}\";\n  return (\"{key:\" + key + \",fn:\" + fn + \"}\")\n}\n\nfunction genForScopedSlot (\n  key,\n  el,\n  state\n) {\n  var exp = el.for;\n  var alias = el.alias;\n  var iterator1 = el.iterator1 ? (\",\" + (el.iterator1)) : '';\n  var iterator2 = el.iterator2 ? (\",\" + (el.iterator2)) : '';\n  el.forProcessed = true; // avoid recursion\n  return \"_l((\" + exp + \"),\" +\n    \"function(\" + alias + iterator1 + iterator2 + \"){\" +\n      \"return \" + (genScopedSlot(key, el, state)) +\n    '})'\n}\n\nfunction genChildren (\n  el,\n  state,\n  checkSkip,\n  altGenElement,\n  altGenNode\n) {\n  var children = el.children;\n  if (children.length) {\n    var el$1 = children[0];\n    // optimize single v-for\n    if (children.length === 1 &&\n      el$1.for &&\n      el$1.tag !== 'template' &&\n      el$1.tag !== 'slot'\n    ) {\n      return (altGenElement || genElement)(el$1, state)\n    }\n    var normalizationType = checkSkip\n      ? getNormalizationType(children, state.maybeComponent)\n      : 0;\n    var gen = altGenNode || genNode;\n    return (\"[\" + (children.map(function (c) { return gen(c, state); }).join(',')) + \"]\" + (normalizationType ? (\",\" + normalizationType) : ''))\n  }\n}\n\n// determine the normalization needed for the children array.\n// 0: no normalization needed\n// 1: simple normalization needed (possible 1-level deep nested array)\n// 2: full normalization needed\nfunction getNormalizationType (\n  children,\n  maybeComponent\n) {\n  var res = 0;\n  for (var i = 0; i < children.length; i++) {\n    var el = children[i];\n    if (el.type !== 1) {\n      continue\n    }\n    if (needsNormalization(el) ||\n        (el.ifConditions && el.ifConditions.some(function (c) { return needsNormalization(c.block); }))) {\n      res = 2;\n      break\n    }\n    if (maybeComponent(el) ||\n        (el.ifConditions && el.ifConditions.some(function (c) { return maybeComponent(c.block); }))) {\n      res = 1;\n    }\n  }\n  return res\n}\n\nfunction needsNormalization (el) {\n  return el.for !== undefined || el.tag === 'template' || el.tag === 'slot'\n}\n\nfunction genNode (node, state) {\n  if (node.type === 1) {\n    return genElement(node, state)\n  } if (node.type === 3 && node.isComment) {\n    return genComment(node)\n  } else {\n    return genText(node)\n  }\n}\n\nfunction genText (text) {\n  return (\"_v(\" + (text.type === 2\n    ? text.expression // no need for () because already wrapped in _s()\n    : transformSpecialNewlines(JSON.stringify(text.text))) + \")\")\n}\n\nfunction genComment (comment) {\n  return (\"_e(\" + (JSON.stringify(comment.text)) + \")\")\n}\n\nfunction genSlot (el, state) {\n  var slotName = el.slotName || '\"default\"';\n  var children = genChildren(el, state);\n  var res = \"_t(\" + slotName + (children ? (\",\" + children) : '');\n  var attrs = el.attrs && (\"{\" + (el.attrs.map(function (a) { return ((camelize(a.name)) + \":\" + (a.value)); }).join(',')) + \"}\");\n  var bind$$1 = el.attrsMap['v-bind'];\n  if ((attrs || bind$$1) && !children) {\n    res += \",null\";\n  }\n  if (attrs) {\n    res += \",\" + attrs;\n  }\n  if (bind$$1) {\n    res += (attrs ? '' : ',null') + \",\" + bind$$1;\n  }\n  return res + ')'\n}\n\n// componentName is el.component, take it as argument to shun flow's pessimistic refinement\nfunction genComponent (\n  componentName,\n  el,\n  state\n) {\n  var children = el.inlineTemplate ? null : genChildren(el, state, true);\n  return (\"_c(\" + componentName + \",\" + (genData$2(el, state)) + (children ? (\",\" + children) : '') + \")\")\n}\n\nfunction genProps (props) {\n  var res = '';\n  for (var i = 0; i < props.length; i++) {\n    var prop = props[i];\n    res += \"\\\"\" + (prop.name) + \"\\\":\" + (transformSpecialNewlines(prop.value)) + \",\";\n  }\n  return res.slice(0, -1)\n}\n\n// #3895, #4268\nfunction transformSpecialNewlines (text) {\n  return text\n    .replace(/\\u2028/g, '\\\\u2028')\n    .replace(/\\u2029/g, '\\\\u2029')\n}\n\n/*  */\n\n// these keywords should not appear inside expressions, but operators like\n// typeof, instanceof and in are allowed\nvar prohibitedKeywordRE = new RegExp('\\\\b' + (\n  'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' +\n  'super,throw,while,yield,delete,export,import,return,switch,default,' +\n  'extends,finally,continue,debugger,function,arguments'\n).split(',').join('\\\\b|\\\\b') + '\\\\b');\n\n// these unary operators should not be used as property/method names\nvar unaryOperatorsRE = new RegExp('\\\\b' + (\n  'delete,typeof,void'\n).split(',').join('\\\\s*\\\\([^\\\\)]*\\\\)|\\\\b') + '\\\\s*\\\\([^\\\\)]*\\\\)');\n\n// strip strings in expressions\nvar stripStringRE = /'(?:[^'\\\\]|\\\\.)*'|\"(?:[^\"\\\\]|\\\\.)*\"|`(?:[^`\\\\]|\\\\.)*\\$\\{|\\}(?:[^`\\\\]|\\\\.)*`|`(?:[^`\\\\]|\\\\.)*`/g;\n\n// detect problematic expressions in a template\nfunction detectErrors (ast) {\n  var errors = [];\n  if (ast) {\n    checkNode(ast, errors);\n  }\n  return errors\n}\n\nfunction checkNode (node, errors) {\n  if (node.type === 1) {\n    for (var name in node.attrsMap) {\n      if (dirRE.test(name)) {\n        var value = node.attrsMap[name];\n        if (value) {\n          if (name === 'v-for') {\n            checkFor(node, (\"v-for=\\\"\" + value + \"\\\"\"), errors);\n          } else if (onRE.test(name)) {\n            checkEvent(value, (name + \"=\\\"\" + value + \"\\\"\"), errors);\n          } else {\n            checkExpression(value, (name + \"=\\\"\" + value + \"\\\"\"), errors);\n          }\n        }\n      }\n    }\n    if (node.children) {\n      for (var i = 0; i < node.children.length; i++) {\n        checkNode(node.children[i], errors);\n      }\n    }\n  } else if (node.type === 2) {\n    checkExpression(node.expression, node.text, errors);\n  }\n}\n\nfunction checkEvent (exp, text, errors) {\n  var stipped = exp.replace(stripStringRE, '');\n  var keywordMatch = stipped.match(unaryOperatorsRE);\n  if (keywordMatch && stipped.charAt(keywordMatch.index - 1) !== '$') {\n    errors.push(\n      \"avoid using JavaScript unary operator as property name: \" +\n      \"\\\"\" + (keywordMatch[0]) + \"\\\" in expression \" + (text.trim())\n    );\n  }\n  checkExpression(exp, text, errors);\n}\n\nfunction checkFor (node, text, errors) {\n  checkExpression(node.for || '', text, errors);\n  checkIdentifier(node.alias, 'v-for alias', text, errors);\n  checkIdentifier(node.iterator1, 'v-for iterator', text, errors);\n  checkIdentifier(node.iterator2, 'v-for iterator', text, errors);\n}\n\nfunction checkIdentifier (\n  ident,\n  type,\n  text,\n  errors\n) {\n  if (typeof ident === 'string') {\n    try {\n      new Function((\"var \" + ident + \"=_\"));\n    } catch (e) {\n      errors.push((\"invalid \" + type + \" \\\"\" + ident + \"\\\" in expression: \" + (text.trim())));\n    }\n  }\n}\n\nfunction checkExpression (exp, text, errors) {\n  try {\n    new Function((\"return \" + exp));\n  } catch (e) {\n    var keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE);\n    if (keywordMatch) {\n      errors.push(\n        \"avoid using JavaScript keyword as property name: \" +\n        \"\\\"\" + (keywordMatch[0]) + \"\\\"\\n  Raw expression: \" + (text.trim())\n      );\n    } else {\n      errors.push(\n        \"invalid expression: \" + (e.message) + \" in\\n\\n\" +\n        \"    \" + exp + \"\\n\\n\" +\n        \"  Raw expression: \" + (text.trim()) + \"\\n\"\n      );\n    }\n  }\n}\n\n/*  */\n\nfunction createFunction (code, errors) {\n  try {\n    return new Function(code)\n  } catch (err) {\n    errors.push({ err: err, code: code });\n    return noop\n  }\n}\n\nfunction createCompileToFunctionFn (compile) {\n  var cache = Object.create(null);\n\n  return function compileToFunctions (\n    template,\n    options,\n    vm\n  ) {\n    options = extend({}, options);\n    var warn$$1 = options.warn || warn;\n    delete options.warn;\n\n    /* istanbul ignore if */\n    {\n      // detect possible CSP restriction\n      try {\n        new Function('return 1');\n      } catch (e) {\n        if (e.toString().match(/unsafe-eval|CSP/)) {\n          warn$$1(\n            'It seems you are using the standalone build of Vue.js in an ' +\n            'environment with Content Security Policy that prohibits unsafe-eval. ' +\n            'The template compiler cannot work in this environment. Consider ' +\n            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +\n            'templates into render functions.'\n          );\n        }\n      }\n    }\n\n    // check cache\n    var key = options.delimiters\n      ? String(options.delimiters) + template\n      : template;\n    if (cache[key]) {\n      return cache[key]\n    }\n\n    // compile\n    var compiled = compile(template, options);\n\n    // check compilation errors/tips\n    {\n      if (compiled.errors && compiled.errors.length) {\n        warn$$1(\n          \"Error compiling template:\\n\\n\" + template + \"\\n\\n\" +\n          compiled.errors.map(function (e) { return (\"- \" + e); }).join('\\n') + '\\n',\n          vm\n        );\n      }\n      if (compiled.tips && compiled.tips.length) {\n        compiled.tips.forEach(function (msg) { return tip(msg, vm); });\n      }\n    }\n\n    // turn code into functions\n    var res = {};\n    var fnGenErrors = [];\n    res.render = createFunction(compiled.render, fnGenErrors);\n    res.staticRenderFns = compiled.staticRenderFns.map(function (code) {\n      return createFunction(code, fnGenErrors)\n    });\n\n    // check function generation errors.\n    // this should only happen if there is a bug in the compiler itself.\n    // mostly for codegen development use\n    /* istanbul ignore if */\n    {\n      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {\n        warn$$1(\n          \"Failed to generate render function:\\n\\n\" +\n          fnGenErrors.map(function (ref) {\n            var err = ref.err;\n            var code = ref.code;\n\n            return ((err.toString()) + \" in\\n\\n\" + code + \"\\n\");\n        }).join('\\n'),\n          vm\n        );\n      }\n    }\n\n    return (cache[key] = res)\n  }\n}\n\n/*  */\n\nfunction createCompilerCreator (baseCompile) {\n  return function createCompiler (baseOptions) {\n    function compile (\n      template,\n      options\n    ) {\n      var finalOptions = Object.create(baseOptions);\n      var errors = [];\n      var tips = [];\n      finalOptions.warn = function (msg, tip) {\n        (tip ? tips : errors).push(msg);\n      };\n\n      if (options) {\n        // merge custom modules\n        if (options.modules) {\n          finalOptions.modules =\n            (baseOptions.modules || []).concat(options.modules);\n        }\n        // merge custom directives\n        if (options.directives) {\n          finalOptions.directives = extend(\n            Object.create(baseOptions.directives),\n            options.directives\n          );\n        }\n        // copy other options\n        for (var key in options) {\n          if (key !== 'modules' && key !== 'directives') {\n            finalOptions[key] = options[key];\n          }\n        }\n      }\n\n      var compiled = baseCompile(template, finalOptions);\n      {\n        errors.push.apply(errors, detectErrors(compiled.ast));\n      }\n      compiled.errors = errors;\n      compiled.tips = tips;\n      return compiled\n    }\n\n    return {\n      compile: compile,\n      compileToFunctions: createCompileToFunctionFn(compile)\n    }\n  }\n}\n\n/*  */\n\n// `createCompilerCreator` allows creating compilers that use alternative\n// parser/optimizer/codegen, e.g the SSR optimizing compiler.\n// Here we just export a default compiler using the default parts.\nvar createCompiler = createCompilerCreator(function baseCompile (\n  template,\n  options\n) {\n  var ast = parse(template.trim(), options);\n  optimize(ast, options);\n  var code = generate(ast, options);\n  return {\n    ast: ast,\n    render: code.render,\n    staticRenderFns: code.staticRenderFns\n  }\n});\n\n/*  */\n\nvar ref$1 = createCompiler(baseOptions);\nvar compileToFunctions = ref$1.compileToFunctions;\n\n/*  */\n\n// check whether current browser encodes a char inside attribute values\nvar div;\nfunction getShouldDecode (href) {\n  div = div || document.createElement('div');\n  div.innerHTML = href ? \"<a href=\\\"\\n\\\"/>\" : \"<div a=\\\"\\n\\\"/>\";\n  return div.innerHTML.indexOf('&#10;') > 0\n}\n\n// #3663: IE encodes newlines inside attribute values while other browsers don't\nvar shouldDecodeNewlines = inBrowser ? getShouldDecode(false) : false;\n// #6828: chrome encodes content in a[href]\nvar shouldDecodeNewlinesForHref = inBrowser ? getShouldDecode(true) : false;\n\n/*  */\n\nvar idToTemplate = cached(function (id) {\n  var el = query(id);\n  return el && el.innerHTML\n});\n\nvar mount = Vue$3.prototype.$mount;\nVue$3.prototype.$mount = function (\n  el,\n  hydrating\n) {\n  el = el && query(el);\n\n  /* istanbul ignore if */\n  if (el === document.body || el === document.documentElement) {\n    \"development\" !== 'production' && warn(\n      \"Do not mount Vue to <html> or <body> - mount to normal elements instead.\"\n    );\n    return this\n  }\n\n  var options = this.$options;\n  // resolve template/el and convert to render function\n  if (!options.render) {\n    var template = options.template;\n    if (template) {\n      if (typeof template === 'string') {\n        if (template.charAt(0) === '#') {\n          template = idToTemplate(template);\n          /* istanbul ignore if */\n          if (\"development\" !== 'production' && !template) {\n            warn(\n              (\"Template element not found or is empty: \" + (options.template)),\n              this\n            );\n          }\n        }\n      } else if (template.nodeType) {\n        template = template.innerHTML;\n      } else {\n        {\n          warn('invalid template option:' + template, this);\n        }\n        return this\n      }\n    } else if (el) {\n      template = getOuterHTML(el);\n    }\n    if (template) {\n      /* istanbul ignore if */\n      if (\"development\" !== 'production' && config.performance && mark) {\n        mark('compile');\n      }\n\n      var ref = compileToFunctions(template, {\n        shouldDecodeNewlines: shouldDecodeNewlines,\n        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,\n        delimiters: options.delimiters,\n        comments: options.comments\n      }, this);\n      var render = ref.render;\n      var staticRenderFns = ref.staticRenderFns;\n      options.render = render;\n      options.staticRenderFns = staticRenderFns;\n\n      /* istanbul ignore if */\n      if (\"development\" !== 'production' && config.performance && mark) {\n        mark('compile end');\n        measure((\"vue \" + (this._name) + \" compile\"), 'compile', 'compile end');\n      }\n    }\n  }\n  return mount.call(this, el, hydrating)\n};\n\n/**\n * Get outerHTML of elements, taking care\n * of SVG elements in IE as well.\n */\nfunction getOuterHTML (el) {\n  if (el.outerHTML) {\n    return el.outerHTML\n  } else {\n    var container = document.createElement('div');\n    container.appendChild(el.cloneNode(true));\n    return container.innerHTML\n  }\n}\n\nVue$3.compile = compileToFunctions;\n\nreturn Vue$3;\n\n})));\n"
  },
  {
    "path": "commons-website/src/test/java/com/terran4j/test/website/HelloController.java",
    "content": "package com.terran4j.test.website;\n\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\n@Controller\npublic class HelloController {\n\n    /**\n     * http://localhost:8080\n     */\n    @RequestMapping(\"/index.html\")\n    @ResponseBody\n    public String hello() {\n        return \"hello, world\";\n    }\n}\n"
  },
  {
    "path": "commons-website/src/test/java/com/terran4j/test/website/MainApp.java",
    "content": "package com.terran4j.test.website;\n\nimport com.terran4j.commons.website.config.WebsiteConfiguration;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Import;\n\n@Import(WebsiteConfiguration.class)\n@SpringBootApplication\npublic class MainApp {\n\n    public static void main(String[] args) {\n        SpringApplication.run(MainApp.class, args);\n    }\n\n}\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.github.terran4j</groupId>\n    <artifactId>terran4j-commons-parent</artifactId>\n    <version>1.0.4-SNAPSHOT</version>\n    <packaging>pom</packaging>\n    <name>terran4j-commons-parent</name>\n    <url>https://github.com/terran4j/commons</url>\n\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.0.5.RELEASE</version>\n    </parent>\n\n    <modules>\n        <module>commons-util</module>\n        <module>commons-website</module>\n        <module>commons-restpack</module>\n        <module>commons-api2doc</module>\n        <module>commons-hedis</module>\n        <!--<module>commons-jfinger</module>-->\n        <module>commons-hi</module>\n        <module>commons-test</module>\n        <module>commons-dsql</module>\n        <module>commons-armq</module>\n        <!--<module>commons-reflux</module>-->\n        <!--<module>commons-api2page</module>-->\n    </modules>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <java.version>1.8</java.version>\n        <jackson.version>2.10.1</jackson.version>\n        <shiro.version>1.2.3</shiro.version>\n    </properties>\n\n    <developers>\n        <developer>\n            <name>terran4j</name>\n            <email>2561813143@qq.com</email>\n            <roles>\n                <role>developer</role>\n            </roles>\n            <timezone>+8</timezone>\n        </developer>\n    </developers>\n    <scm>\n        <connection>scm:git:https://github.com/terran4j/commons.git</connection>\n        <developerConnection>scm:git:https://github.com/terran4j/commons.git</developerConnection>\n        <url>https://github.com/terran4j/commons</url>\n        <tag>${project.version}</tag>\n    </scm>\n\n    <dependencyManagement>\n        <dependencies>\n            <!-- terran4j -->\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-api2doc</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-util</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-jfinger</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-hi</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-restpack</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-reflux</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-hedis</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-test</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-website</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.terran4j</groupId>\n                <artifactId>terran4j-commons-dsql</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>com.vladsch.flexmark</groupId>\n                <artifactId>flexmark-all</artifactId>\n                <version>0.26.4</version>\n            </dependency>\n            <!--markdown to html -->\n            <dependency>\n                <groupId>com.vladsch.flexmark</groupId>\n                <artifactId>flexmark</artifactId>\n                <version>0.26.4</version>\n            </dependency>\n            <dependency>\n                <groupId>com.vladsch.flexmark</groupId>\n                <artifactId>flexmark-util</artifactId>\n                <version>0.26.4</version>\n            </dependency>\n            <!--表格渲染插件 -->\n            <dependency>\n                <groupId>com.vladsch.flexmark</groupId>\n                <artifactId>flexmark-ext-tables</artifactId>\n                <version>0.26.4</version>\n            </dependency>\n            <dependency>\n                <groupId>com.vladsch.flexmark</groupId>\n                <artifactId>flexmark-ext-spec-example</artifactId>\n                <version>0.26.4</version>\n            </dependency>\n\n            <!-- test -->\n            <dependency>\n                <groupId>junit</groupId>\n                <artifactId>junit</artifactId>\n                <version>4.12</version>\n            </dependency>\n            <dependency>\n                <groupId>org.mockito</groupId>\n                <artifactId>mockito-all</artifactId>\n                <version>1.10.19</version>\n            </dependency>\n\n            <!-- google -->\n            <dependency>\n                <groupId>com.google.gson</groupId>\n                <artifactId>gson</artifactId>\n                <version>2.7</version>\n            </dependency>\n            <dependency>\n                <groupId>com.google.guava</groupId>\n                <artifactId>guava</artifactId>\n                <version>21.0</version>\n            </dependency>\n\n            <!-- apache common -->\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-lang3</artifactId>\n                <version>3.5</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-collections4</artifactId>\n                <version>4.1</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-email</artifactId>\n                <version>1.4</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.httpcomponents</groupId>\n                <artifactId>httpclient</artifactId>\n                <version>4.4.1</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.poi</groupId>\n                <artifactId>poi</artifactId>\n                <version>3.15</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.poi</groupId>\n                <artifactId>poi-ooxml</artifactId>\n                <version>3.15</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.poi</groupId>\n                <artifactId>poi-ooxml-schemas</artifactId>\n                <version>3.15</version>\n            </dependency>\n            <dependency>\n                <groupId>commons-io</groupId>\n                <artifactId>commons-io</artifactId>\n                <version>2.4</version>\n            </dependency>\n            <dependency>\n                <groupId>commons-fileupload</groupId>\n                <artifactId>commons-fileupload</artifactId>\n                <version>1.3.1</version>\n            </dependency>\n            <dependency>\n                <groupId>commons-codec</groupId>\n                <artifactId>commons-codec</artifactId>\n                <version>1.3</version>\n            </dependency>\n\n            <!-- mybatis -->\n            <dependency>\n                <groupId>org.mybatis</groupId>\n                <artifactId>mybatis</artifactId>\n                <version>3.4.0</version>\n            </dependency>\n            <dependency>\n                <groupId>org.mybatis</groupId>\n                <artifactId>mybatis-spring</artifactId>\n                <version>1.3.0</version>\n            </dependency>\n            <dependency>\n                <groupId>joda-time</groupId>\n                <artifactId>joda-time</artifactId>\n                <version>2.9.1</version>\n            </dependency>\n            <dependency>\n                <groupId>com.zaxxer</groupId>\n                <artifactId>HikariCP</artifactId>\n                <version>2.4.7</version>\n            </dependency>\n\n            <!-- jackon -->\n            <dependency>\n                <groupId>com.fasterxml.jackson.core</groupId>\n                <artifactId>jackson-core</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.core</groupId>\n                <artifactId>jackson-annotations</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.core</groupId>\n                <artifactId>jackson-databind</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.dataformat</groupId>\n                <artifactId>jackson-dataformat-xml</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.datatype</groupId>\n                <artifactId>jackson-datatype-guava</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.module</groupId>\n                <artifactId>jackson-module-jsonSchema</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.codehaus.jackson</groupId>\n                <artifactId>jackson-core-asl</artifactId>\n                <version>1.9.1</version>\n            </dependency>\n            <dependency>\n                <groupId>org.codehaus.jackson</groupId>\n                <artifactId>jackson-mapper-asl</artifactId>\n                <version>1.9.1</version>\n            </dependency>\n\n            <!-- shiro -->\n            <dependency>\n                <groupId>org.apache.shiro</groupId>\n                <artifactId>shiro-core</artifactId>\n                <version>${shiro.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.shiro</groupId>\n                <artifactId>shiro-web</artifactId>\n                <version>${shiro.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.shiro</groupId>\n                <artifactId>shiro-aspectj</artifactId>\n                <version>${shiro.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.shiro</groupId>\n                <artifactId>shiro-ehcache</artifactId>\n                <version>${shiro.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.shiro</groupId>\n                <artifactId>shiro-spring</artifactId>\n                <version>${shiro.version}</version>\n            </dependency>\n\n            <!-- 将自增id转成不易猜测的字符串 -->\n            <dependency>\n                <groupId>org.hashids</groupId>\n                <artifactId>hashids</artifactId>\n                <version>1.0.1</version>\n            </dependency>\n\n            <!-- 安全算法工具库 -->\n            <dependency>\n                <groupId>org.bouncycastle</groupId>\n                <artifactId>bcprov-jdk15on</artifactId>\n                <version>1.56</version>\n            </dependency>\n\n            <!-- sms -->\n            <dependency>\n                <groupId>commons-httpclient</groupId>\n                <artifactId>commons-httpclient</artifactId>\n                <version>3.1</version>\n            </dependency>\n\n            <!-- Aspect-J -->\n            <dependency>\n                <groupId>aspectj</groupId>\n                <artifactId>aspectjrt</artifactId>\n                <version>1.5.4</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.redisson</groupId>\n                <artifactId>redisson</artifactId>\n                <version>3.6.5</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.projectlombok</groupId>\n                <artifactId>lombok</artifactId>\n                <version>1.18.8</version>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter</artifactId>\n        </dependency>\n\n        <!-- 测试支持。 -->\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n        </dependency>\n\n        <!-- 支持 lombok -->\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <version>1.18.8</version>\n        </dependency>\n        <!--<dependency>-->\n        <!--<groupId>org.jetbrains.kotlin</groupId>-->\n        <!--<artifactId>kotlin-stdlib-jdk8</artifactId>-->\n        <!--<version>${kotlin.version}</version>-->\n        <!--</dependency>-->\n        <!--<dependency>-->\n        <!--<groupId>org.jetbrains.kotlin</groupId>-->\n        <!--<artifactId>kotlin-test</artifactId>-->\n        <!--<version>${kotlin.version}</version>-->\n        <!--<scope>test</scope>-->\n        <!--</dependency>-->\n    </dependencies>\n\n\n    <distributionManagement>\n        <!-- SNAPSHOT版本，部署到私服  -->\n        <snapshotRepository>\n            <id>terran4j-snapshots</id>\n            <name>terran4j-snapshots</name>\n            <url>${terran4j.snapshots.url}</url>\n        </snapshotRepository>\n\n        <!-- 正式版本，部署到中央仓库 -->\n        <repository>\n            <id>ossrh</id>\n            <name>Maven Central Staging Repository</name>\n            <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n        </repository>\n    </distributionManagement>\n\n    <build>\n        <resources>\n            <resource>\n                <directory>src/main/java</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <directory>src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n        <testResources>\n            <testResource>\n                <directory>src/test/java</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </testResource>\n            <testResource>\n                <directory>src/test/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </testResource>\n        </testResources>\n\n        <plugins>\n            <!-- 生成 java 源代码包。 -->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>attach-sources</id>\n                        <goals>\n                            <goal>jar-no-fork</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>release</id>\n            <!--\n                构建 snapshot 版本时不要用这些插件；\n                需要发布 release 版时再用。\n            -->\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.sonatype.plugins</groupId>\n                        <artifactId>nexus-staging-maven-plugin</artifactId>\n                        <version>1.6.3</version>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <serverId>ossrh</serverId>\n                            <nexusUrl>https://oss.sonatype.org/</nexusUrl>\n                            <autoReleaseAfterClose>true</autoReleaseAfterClose>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <version>2.10.3</version>\n                        <executions>\n                            <execution>\n                                <id>attach-javadocs</id>\n                                <goals>\n                                    <goal>jar</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <version>1.6</version>\n                        <executions>\n                            <execution>\n                                <id>sign-artifacts</id>\n                                <phase>verify</phase>\n                                <goals>\n                                    <goal>sign</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n\n</project>"
  },
  {
    "path": "version.md",
    "content": "\n## 版本说明\n\n本项目及所有子项目，版本均是统一升级的，\n当前最新稳定版为：  **1.0.1**"
  }
]