├── .buildscript ├── deploy_snapshot.sh └── settings.xml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── annotation ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── hannesdorfmann │ └── fragmentargs │ ├── FragmentArgs.java │ ├── FragmentArgsInjector.java │ ├── annotation │ ├── Arg.java │ └── FragmentWithArgs.java │ └── bundler │ ├── ArgsBundler.java │ ├── CastedArrayListArgsBundler.java │ └── NoneArgsBundler.java ├── bundler-parceler ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── hannesdorfmann │ └── fragmentargs │ └── bundler │ └── ParcelerArgsBundler.java ├── checkstyle.xml ├── github-upload.rb ├── pom.xml └── processor ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── hannesdorfmann │ │ └── fragmentargs │ │ ├── processor │ │ ├── AnnotatedFragment.java │ │ ├── ArgProcessor.java │ │ ├── ArgumentAnnotatedField.java │ │ ├── ModifierUtils.java │ │ └── ProcessingException.java │ │ └── repacked │ │ └── com │ │ └── squareup │ │ └── javawriter │ │ └── JavaWriter.java └── resources │ └── META-INF │ └── services │ └── javax.annotation.processing.Processor └── test ├── java └── com │ └── hannesdorfmann │ └── fragmentargs │ └── processor │ ├── CompareModifierUtilsTest.java │ ├── CompileTest.java │ ├── InnerClassTest.java │ ├── ModifierUtilsDefaultModifierTest.java │ └── ProtectedAccessTest.java └── resources ├── ClassWithInnerClass.java ├── ClassWithInnerClassBuilder.java ├── ClassWithProtectedField.java ├── ClassWithProtectedSetter.java ├── InnerClassWithProtectedField.java └── InnerClassWithProtectedFieldBuilder.java /.buildscript/deploy_snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo. 4 | # 5 | # Adapted from https://coderwall.com/p/9b_lfq and 6 | # http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/ 7 | 8 | SLUG="sockeqwe/fragmentargs" 9 | JDK="oraclejdk7" 10 | BRANCH="master" 11 | 12 | set -e 13 | echo "starting deploy snapshot script" 14 | 15 | if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then 16 | echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'." 17 | elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then 18 | echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'." 19 | elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 20 | echo "Skipping snapshot deployment: was pull request." 21 | elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then 22 | echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'." 23 | else 24 | echo "Deploying snapshot..." 25 | mvn clean source:jar javadoc:jar deploy --settings=".buildscript/settings.xml" -Dmaven.test.skip=true 26 | echo "Snapshot deployed!" 27 | fi -------------------------------------------------------------------------------- /.buildscript/settings.xml: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | sonatype-nexus-snapshots 23 | ${env.CI_DEPLOY_USERNAME} 24 | ${env.CI_DEPLOY_PASSWORD} 25 | 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .factorypath 3 | .project 4 | .settings 5 | eclipsebin 6 | 7 | bin 8 | gen 9 | build 10 | out 11 | lib 12 | 13 | target 14 | pom.xml.* 15 | release.properties 16 | log 17 | *.versionsBackup 18 | 19 | .idea 20 | *.iml 21 | *.iws 22 | classes 23 | 24 | obj 25 | 26 | .DS_Store 27 | *.DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | before_install: 3 | - chmod +x .buildscript/deploy_snapshot.sh 4 | after_success: 5 | - ".buildscript/deploy_snapshot.sh" 6 | notifications: 7 | email: false 8 | jdk: 9 | - oraclejdk8 10 | sudo: false 11 | cache: 12 | directories: 13 | - "$HOME/.m2/io" 14 | - "$HOME/.m2/org" 15 | env: 16 | global: 17 | - secure: elVniBqisDQgH6JJb/2t2PLxthjfMH9UiJpkQzn0KAFkCW5OzqhR36T2xeaeP+s4lgCmFKRojr7vcT0zOU4BvtS4wyrUJf971UJ/XWJYyCfR21xWwjeBuvIR8nsx2+r+TDz2hwfLfJ0GsVkaxxu63wegA5qjmfZGUlwC3tCwQWY= 18 | - secure: Fq+HfYIx3+tJcuL+O4hoyChrb8ri4Kbmu7qE+TSw/Hpq8g/a6VMXb9pca72MCdh1Ucb/94R6ISk8pfPL1IBB9aFgqhahQgArGkD9mV4UcpPFyB2OwchT0G7eceO2oMPG/kyL6hM4/nkIqUCrrNT0MFvPD/b6iwzDzEnuXNmOGyk= 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.0.0-RC1 (2018-06-20): 2 | - Next major release 3 | - Nullability in Fragment Builder: Non Optional arguments must not be null. Corresponding @Nullable annotations are applied. 4 | - Support for `anroidx.Fragment` [#103](https://github.com/sockeqwe/fragmentargs/pull/103) 5 | - Some minor improvements: 6 | 1. [Missing @override annotation](https://github.com/sockeqwe/fragmentargs/pull/86) 7 | 2. [If POJO implements Parcelable and Serializeable, prefer Parcelable](https://github.com/sockeqwe/fragmentargs/issues/74) 8 | 3. [Support for protected fields](https://github.com/sockeqwe/fragmentargs/issues/61) 9 | 4. [Create bundle via Builder](https://github.com/sockeqwe/fragmentargs/issues/58) 10 | 5. [Better support for boolean args in kotlin](https://github.com/sockeqwe/fragmentargs/pull/100) 11 | 6. [Annotation Processing option to suppress warnings](https://github.com/sockeqwe/fragmentargs/pull/98) 12 | 13 | # 3.0.2 (2016-03--04): 14 | - Hotfix (#47): There was a bug that Builder has not been generated when Fragment have no (zero) arguments 15 | 16 | # 3.0.1 (2015-12-28): 17 | - Minor Bugfix (#35): when using custom `Bundler` and setter methods (because annotated field is private) java compiler could not determine the generic type of setter method parameter. 18 | 19 | # 3.0.0 (2015-11-02): See [this](http://hannesdorfmann.com/android/fragmentargs3/) blog post for more information 20 | - You now have to annotate the Fragment class itself with `@FragmentWithArgs`. For backward compatibility reasons this is not mandatory. However it's strongly recommended because in further versions of FragmentArgs this could become mandatory to support more features. 21 | - Deprecated `@FragmentArgsInherited`. Use `@FragmentWithArgs(inherited = true or false)` instead. 22 | - Support for setter methods: Still annotate your fields with `@Arg` not the setter method. Now you can annotate `private` fields as well, but you have to provide the corresponding setter method. 23 | - Kotlin support: Since setter methods are now supported, FragmentArgs support kotlin backing fields out of the box. 24 | - Generated Builder classes are now per default annotated with `@NonNull` from androids support annotation library. Furthermore, this adds even better kotlin support since kotlin uses this annotations for null safety. You can disable this option by using annotation processor option `fragmentArgsSupportAnnotations false`. See readme for detail information. 25 | - You can use annotation processor option `fragmentArgsBuilderAnnotations "com.example.MyAnnotation"` to annotate the generated builder classes with additional third party annotations. See readme for details information. 26 | 27 | # 2.1.0 (2015-05-01) 28 | - Added `ArgsBundler` to provide a plugin mechanism for not out of the box supported data types. Two ArgsBundler are already provided `CastedArrayListArgsBundler` and `PacelerArgsBundler`. 29 | - Removed warning: "Could not load the generated automapping class. However, that may be ok, if you use FragmentArgs in library projects". 30 | - Better error messages if annotating `@Arg` on unsupported type fields. 31 | 32 | # 2.0.1 (2014-12-22) 33 | - Removed the compilation warning: `Warning: The following options were not recognized by any processor: '[fragmentArgsLib]'` 34 | - Minor bugfix that have occurred on some java 6 enviroments in combination with `@FragmentArgsInherited` 35 | 36 | # 2.0.0 (2014-12-10) 37 | - Support for inheritance through included modules, jar and aar 38 | - Introduced `@FragmentArgsInherited` annotation 39 | 40 | # 1.2.0 (2014-12-06) 41 | Better support for inheritance and abstract classes 42 | 43 | # 1.1.0 (2014-11-12) 44 | Support for Android libraries to avoid errors where the auto injector class is generated multiple times which will cause this error 45 | `Multiple dex files define com/hannesdorfmann/fragmentargs/AutoFragmentArgInjector` 46 | 47 | # 1.0.3 (2014-09-27) 48 | optimization 49 | 50 | # 1.0.2 (2014-09-22) 51 | Minor bug fix 52 | 53 | # 1.0.1 (2014-09-16) 54 | Annotation Processor: support for Java 7 and Java 8 55 | 56 | 57 | # 1.0.0 (2014-09-15) 58 | First major release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Hannes Dorfmann 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FragmentArgs 2 | Annotation Processor to create arguments for android fragments without using reflections. 3 | 4 | I have written a blog entry about this library: http://hannesdorfmann.com/android/fragmentargs 5 | 6 | # Dependency 7 | Latest version: [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.hannesdorfmann.fragmentargs/annotation/badge.png)](https://maven-badges.herokuapp.com/maven-central/com.hannesdorfmann.fragmentargs/annotation) 8 | [![Build Status](https://travis-ci.org/sockeqwe/fragmentargs.svg?branch=master)](https://travis-ci.org/sockeqwe/fragmentargs) 9 | 10 | ```groovy 11 | dependencies { 12 | compile 'com.hannesdorfmann.fragmentargs:annotation:4.0.0-RC1' 13 | annotationProcessor 'com.hannesdorfmann.fragmentargs:processor:4.0.0-RC1' 14 | } 15 | ``` 16 | ### SNAPSHOT 17 | Lastest snapshot version is `4.0.0-SNAPSHOT`. You also have to add the url to the snapshot repository: 18 | 19 | ```gradle 20 | allprojects { 21 | repositories { 22 | ... 23 | 24 | maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } 25 | } 26 | ``` 27 | 28 | 29 | # Changelog 30 | The changelog can be found [here](https://github.com/sockeqwe/fragmentargs/blob/master/CHANGELOG.md) 31 | 32 | # How to use 33 | FragmentArgs generates Java code at compile time. It generates a `Builder` class out of your Fragment class. 34 | 35 | 1. Annotate your `Fragment` with `@FragmentWithArgs`. For backward compatibility reasons this is not mandatory. However it's strongly recommended because in further versions of FragmentArgs this could become mandatory to support more features. 36 | 2. Annotate your fields with `@Arg`. Fields **should** have at least package (default) visibility. Alternatively, you have to provide a setter method with at least package (default) visibility for your private `@Arg` annotated fields. 37 | 3. In the Fragments `onCreate(Bundle)` method you have to call `FragmentArgs.inject(this)` to read the arguments and set the values. 38 | 4. Unlike Eclipse Android Studio does not auto compile your project while saving files. So you may have to build your project to start the annotation processor which will generate the `Builder` classes for your annotated fragments. 39 | 40 | Example: 41 | 42 | ```java 43 | import com.hannesdorfmann.fragmentargs.FragmentArgs; 44 | import com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs; 45 | import com.hannesdorfmann.fragmentargs.annotation.Arg; 46 | 47 | @FragmentWithArgs 48 | public class MyFragment extends Fragment { 49 | 50 | @Arg 51 | int id; 52 | 53 | @Arg 54 | private String title; // private fields requires a setter method 55 | 56 | @Override 57 | public void onCreate(Bundle savedInstanceState) { 58 | super.onCreate(savedInstanceState); 59 | FragmentArgs.inject(this); // read @Arg fields 60 | } 61 | 62 | @Override 63 | public View onCreateView(LayoutInflater inflater, 64 | ViewGroup container, Bundle savedInstanceState) { 65 | 66 | Toast.makeText(getActivity(), "Hello " + title, 67 | Toast.LENGTH_SHORT).show(); 68 | 69 | return null; 70 | } 71 | 72 | // Setter method for private field 73 | public void setTitle(String title) { 74 | this.title = title; 75 | } 76 | 77 | } 78 | ``` 79 | 80 | In your Activity you will use the generated `Builder` class _(the name of your fragment with "Builder" suffix)_ instead of `new MyFragment()` or a static `MyFragment.newInstance(int id, String title)` method. 81 | 82 | For example: 83 | 84 | ```java 85 | public class MyActivity extends Activity { 86 | 87 | public void onCreate(Bundle savedInstanceState){ 88 | super.onCreate(savedInstanceState); 89 | 90 | int id = 123; 91 | String title = "test"; 92 | 93 | // Using the generated Builder 94 | Fragment fragment = 95 | new MyFragmentBuilder(id, title) 96 | .build(); 97 | 98 | 99 | // Fragment Transaction 100 | getFragmentManager() 101 | .beginTransaction() 102 | .replace(R.id.container, fragment) 103 | .commit(); 104 | 105 | } 106 | 107 | } 108 | ``` 109 | 110 | ## Optional Arguments 111 | You can specify a fragment argument to be optional by using `@Arg(required = false)` 112 | 113 | For example: 114 | ```java 115 | @FragmentWithArgs 116 | public class MyOptionalFragment extends Fragment { 117 | 118 | @Arg 119 | int id; 120 | 121 | @Arg 122 | String title; 123 | 124 | @Arg(required = false) 125 | String additionalText; 126 | 127 | @Arg(required = false) 128 | float factor; 129 | 130 | @Arg(required = false) 131 | int mFeatureId; 132 | 133 | @Override 134 | public void onCreate(Bundle savedInstanceState){ 135 | super.onCreate(savedInstanceState); 136 | FragmentArgs.inject(this); // read @Arg fields 137 | } 138 | 139 | } 140 | ``` 141 | 142 | Optional arguments will generate a `Builder` class with additional methods to set optional arguments. 143 | 144 | For Example: 145 | ```java 146 | public class MyActivity extends Activity { 147 | 148 | public void onCreate(Bundle savedInstanceState){ 149 | super.onCreate(savedInstanceState); 150 | 151 | int id = 123; 152 | String title = "test"; 153 | 154 | // Using the generated Builder 155 | Fragment fragment = 156 | new MyFragmentBuilder(id, title) // required args 157 | .additionalText("foo") // Optional arg 158 | .factor(1.2f) // Optional arg 159 | .featureId(42) // Optional arg 160 | .build(); 161 | 162 | 163 | // Fragment Transaction 164 | getFragmentManager() 165 | .beginTransaction() 166 | .replace(R.id.container, fragment) 167 | .commit(); 168 | } 169 | 170 | } 171 | ``` 172 | 173 | As you have seen optional fragment arguments are part of the `Builder` class as an own methods. Since they are optional you can decide if you want to set optional values or not by calling the corresponding method or skip the corresponding method call. 174 | 175 | Like you have seen from the example above fields named with "m" prefix will be automatically cut by making the method name the sub-string of the original fields name without the "m" prefix. For example the field `int mFeatureId` corresponds to the builders method `featureId(int)` 176 | 177 | ## Inheritance - Best practice 178 | Wouldn't it be painful to override `onCreate(Bundle)` in every Fragment of your app just to insert `FragmentArgs.inject(this)`. 179 | FragmentArgs are designed to support inheritance. Hence you can override once `onCreate(Bundle)` in your Fragment base class and do not need to override this for every single Fragment. 180 | 181 | For example: 182 | ```java 183 | public class BaseFragment extends Fragment { 184 | 185 | @Override 186 | public void onCreate(Bundle savedInstanceState){ 187 | super.onCreate(savedInstanceState); 188 | FragmentArgs.inject(this); // read @Arg fields 189 | } 190 | } 191 | ``` 192 | 193 | ```java 194 | @FragmentWithArgs 195 | public class MyFragment extends BaseFragment { 196 | 197 | @Arg 198 | String title; 199 | 200 | @Override 201 | public View onCreateView(LayoutInflater inflater, 202 | ViewGroup container, Bundle savedInstanceState) { 203 | 204 | Toast.makeText(getActivity(), "Hello " + title, 205 | Toast.LENGTH_SHORT).show(); 206 | } 207 | } 208 | ``` 209 | 210 | ```java 211 | @FragmentWithArgs 212 | public class OtherFragment extends BaseFragment { 213 | 214 | @Arg 215 | String foo; 216 | 217 | @Override 218 | public View onCreateView(LayoutInflater inflater, 219 | ViewGroup container, Bundle savedInstanceState) { 220 | 221 | Toast.makeText(getActivity(), "Hello " + foo, 222 | Toast.LENGTH_SHORT).show(); 223 | } 224 | } 225 | ``` 226 | `FragmentArgs` also supports inheritance and abstract classes. That means that annotated fields of the supper class are part of the builder of the subclass. Furthermore this also works for special cases where you have a Fragment without any `@Arg` annotation but you want to use the arguments of the super class. For Example: 227 | 228 | ```java 229 | public class A extends Fragment { 230 | 231 | @Arg int a; 232 | @Arg String foo; 233 | 234 | } 235 | 236 | @FragmentArgs 237 | public class B extends A { 238 | 239 | // Arguments will be taken from super class 240 | 241 | 242 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 243 | Bundle savedInstanceState) { 244 | 245 | // Here you can simply access the inherited fields from super class 246 | } 247 | } 248 | ``` 249 | 250 | There may be special edge cases where you don't want to use the fragment args from super class. Then you can use `@FragmentWithArgs(inherited = false)`. Example: 251 | ```java 252 | @FragmentWithArgs(inherited = false) 253 | public class C extends A { 254 | 255 | @Arg int c; 256 | 257 | } 258 | ``` 259 | 260 | In this case only `c` will be argument of class C and the arguments of super class A are ignored. 261 | 262 | # ArgsBundler 263 | FragmentArgs supports the most common data structures that you can put in a `Bundle` and hence set as arguments for a Fragment. The type of the `@Arg` annotated field is used for that. If you want to set not a out of the box supported data type (like a class you cant make `Parcelable` for whatever reason) as argument you can specify your own `ArgsBundler`. 264 | 265 | ```java 266 | public class DateArgsBundler implements ArgsBundler{ 267 | 268 | @Override public void put(String key, Date value, Bundle bundle) { 269 | 270 | bundle.putLong(key, value.getTime()); 271 | } 272 | 273 | @Override public Date get(String key, Bundle bundle) { 274 | 275 | long timestamp = bundle.getLong(key); 276 | return new Date(timestamp); 277 | } 278 | 279 | } 280 | 281 | public class MyFragment extends Fragment { 282 | 283 | @Arg ( bundler = DateArgsBundler.class ) 284 | Date date; 285 | 286 | } 287 | ``` 288 | 289 | There are already two `ArgBundler` you may find useful: 290 | ```java 291 | @FragmentWithArgs 292 | public class MyFragment { 293 | 294 | @Arg ( bundler = CastedArrayListArgsBundler.class ) 295 | List fooList; // Foo implements Parcelable 296 | 297 | @Arg ( bundler = ParcelerArgsBundler.class) 298 | Dog dog; // Dog is @Parcel annotated 299 | } 300 | ``` 301 | 302 | - `CastedArrayListArgsBundler`: The problem is that in a Bundle supports `java.util.ArrayList` and not `java.util.List`. `CastedArrayListArgsBundler` assumes that the List implementation is `ArrayList` and casts `List` internally to `ArrayList` and put it into a bundle. 303 | 304 | - If you use [Parceler](http://parceler.org/) then you may know that your `@Parcel` annotated class is not implemnting `Parcelable` directly (Parceler generates a wrapper for your class that implements Parcelable). Therefore a `@Parcel` class can not be set directly as fragment argument with `@Arg`. However, there is a ArgsBundler called `ParcelerArgsBundler` that you can use with `@Parcel`. 305 | 306 | ```java 307 | @Parcel 308 | public class Dog { 309 | String name; 310 | } 311 | 312 | 313 | public class MyFragment { 314 | 315 | @Arg ( bundler = ParcelerArgsBundler.class ) 316 | Dog foo; 317 | 318 | } 319 | 320 | ``` 321 | 322 | While `CastedArrayListArgsBundler` already ships with `compile 'com.hannesdorfmann.fragmentargs:annotation:x.x.x' ` you have to add 323 | 324 | ``` groovy 325 | compile 'com.hannesdorfmann.fragmentargs:bundler-parceler:x.x.x' 326 | ``` 327 | as dependency to use `ParcelerArgsBundler`. 328 | 329 | # Kotlin support 330 | As starting with `FragmentArgs 3.0.0` the kotlin programming language is supported (use `kapt` instead of `apt`): 331 | 332 | ```kotlin 333 | @FragmentWithArgs 334 | class KotlinFragment : Fragment() { 335 | 336 | @Arg var foo: String = "foo" 337 | @Arg(required = false) lateinit var bar: String // works also with lateinit for non primitives 338 | 339 | override fun onCreate(savedInstanceState: Bundle?) { 340 | super.onCreate(savedInstanceState) 341 | FragmentArgs.inject(this) 342 | } 343 | 344 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 345 | val view = inflater.inflate(R.layout.fragment_kotlin, container, false) 346 | 347 | val tv = view.findViewById(R.id.textView) as TextView 348 | 349 | tv.text = "Foo = ${foo} , bar = ${bar}" 350 | return view; 351 | } 352 | } 353 | ``` 354 | 355 | # Support Fragment 356 | Fragments of the support library are supported. Therefore fields in `android.support.v4.app.Fragment` or `android.app.Fragment` can be annotated with `@Arg`. 357 | 358 | 359 | # Using in library projects 360 | You can use FragmentArgs in library projects. However, in library project you have to inject the arguments by hand in each Fragment. First of all, you have to specify in your libraries `build.gradle` that FragmentArgs should treat this project as a library project by adding the following lines: 361 | 362 | ```groovy 363 | apply plugin: 'com.android.library' 364 | apply plugin: 'com.neenbedankt.android-apt' 365 | 366 | // Options for annotation processor 367 | apt { 368 | arguments { 369 | fragmentArgsLib true 370 | } 371 | } 372 | 373 | android { 374 | ... 375 | } 376 | 377 | dependencies { 378 | compile 'com.hannesdorfmann.fragmentargs:annotation:x.x.x' 379 | apt 'com.hannesdorfmann.fragmentargs:processor:x.x.x' 380 | ... 381 | } 382 | ``` 383 | 384 | So the important thing is `fragmentArgsLib = true`. Otherwise you will get an compile error like this 385 | `Multiple dex files define com/hannesdorfmann/fragmentargs/AutoFragmentArgInjector` in your app project that uses FrgmentArgs and your library (which uses FragmentArgs as well). 386 | 387 | Next you have to manually inject the FragmentArguments in your Fragment which is part of your library. So you **can not use** `FragmentArgs.inject()` but you have to use explicit the generated FragmentBuilder class. Example: 388 | ```java 389 | @FragmentWithArgs 390 | public class FragmenInLib extends Fragment { 391 | 392 | @Arg String foo; 393 | @Arg int test; 394 | 395 | @Override 396 | public void onCreate(Bundle savedInstanceState) { 397 | super.onCreate(savedInstanceState); 398 | 399 | // Use the generated builder class to "inject" the arguments on creation 400 | FragmenInLibBuilder.injectArguments(this); 401 | } 402 | 403 | } 404 | 405 | ``` 406 | 407 | # Annotation Processor Options 408 | The FragmentArgs annotation processor supports some options for customization. 409 | 410 | ```groovy 411 | // Hugo Visser's APT plugin 412 | apt { 413 | arguments { 414 | fragmentArgsLib true 415 | fragmentArgsSupportAnnotations false 416 | fragmentArgsBuilderAnnotations "hugo.weaving.DebugLog com.foo.OtherAnnotation" 417 | fragmentArgsLogWarnings false // Don't print warnings 418 | } 419 | } 420 | 421 | 422 | // Annotation processor 423 | android { 424 | .... 425 | defaultConfig { 426 | .... 427 | javaCompileOptions { 428 | annotationProcessorOptions { 429 | arguments = [ fragmentArgsLib : 'true' ] 430 | includeCompileClasspath true 431 | } 432 | } 433 | } 434 | } 435 | // Kotlin Annotation processor 436 | kapt { 437 | generateStubs = true 438 | arguments { 439 | arg("fragmentArgsLib", true) 440 | arg("fragmentArgsSupportAnnotations", false) 441 | arg("fragmentArgsBuilderAnnotations", "hugo.weaving.DebugLog com.foo.OtherAnnotation") 442 | arg("fragmentArgsLogWarnings", false) // Don't print warnings 443 | } 444 | } 445 | ``` 446 | 447 | - **fragmentArgsLib**: Already described in _"Using in library projects"_ 448 | - **fragmentArgsSupportAnnotations**: As default the methods of the generated `Builder` are annotated with the annotations from support library like `@NonNull` etc. You can disable that feature by passing `false`. 449 | - **fragmentArgsBuilderAnnotations**: You can add additional annotations to the generated `Builder` classes. For example you can add `@DebugLog` annotation to the `Builder` classes to use Jake Wharton's [Hugo](https://github.com/JakeWharton/hugo) for logging in debug builds. You have to pass a string of a full qualified annotation class name. You can supply multiple annotations by using a white space between each one. 450 | - **fragmentArgsLogWarnings**: You can disable all `warning` logs with this flag. (e.g. `warning: {enumFieldName} will be stored as Serializable`) 451 | 452 | # Proguard 453 | ``` 454 | -keep class com.hannesdorfmann.fragmentargs.** { *; } 455 | ``` 456 | 457 | # Thanks 458 | Parts of the annotation code are based on Hugo Visser's [Bundle](https://bitbucket.org/hvisser/bundles) project. I have added some optimizations and improvements. 459 | -------------------------------------------------------------------------------- /annotation/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fragmentargs-parent 7 | com.hannesdorfmann.fragmentargs 8 | 4.0.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | 13 | annotation 14 | 15 | 16 | 17 | com.google.android 18 | android 19 | provided 20 | 21 | 22 | 23 | junit 24 | junit 25 | test 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | org.apache.maven.plugins 34 | maven-javadoc-plugin 35 | 36 | 37 | attach-javadocs 38 | 39 | jar 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-source-plugin 48 | 49 | 50 | attach-sources 51 | 52 | jar 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/hannesdorfmann/fragmentargs/FragmentArgs.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs; 2 | 3 | /** 4 | * The root class to inject arguments to a fragment 5 | * 6 | * @author Hannes Dorfmann 7 | */ 8 | public class FragmentArgs { 9 | 10 | public static final String AUTO_MAPPING_CLASS_NAME = "AutoFragmentArgInjector"; 11 | public static final String AUTO_MAPPING_PACKAGE = "com.hannesdorfmann.fragmentargs"; 12 | public static final String AUTO_MAPPING_QUALIFIED_CLASS = 13 | AUTO_MAPPING_PACKAGE + "." + AUTO_MAPPING_CLASS_NAME; 14 | 15 | private static FragmentArgsInjector autoMappingInjector; 16 | 17 | public static void inject(Object fragment) { 18 | injectFromBundle(fragment); 19 | } 20 | 21 | static void injectFromBundle(Object target) { 22 | 23 | if (autoMappingInjector == null) { 24 | // Load the automapping class 25 | try { 26 | Class c = Class.forName(AUTO_MAPPING_QUALIFIED_CLASS); 27 | autoMappingInjector = (FragmentArgsInjector) c.newInstance(); 28 | } catch (Exception e) { 29 | // Since 2.0.0 we don't throw an exception because of android library support. 30 | // Instead we print this exception as warning message 31 | 32 | /* 33 | Exception wrapped = new Exception("Could not load the generated automapping class. " 34 | + "However, that may be ok, if you use FragmentArgs in library projects", e); 35 | wrapped.printStackTrace(); 36 | */ 37 | } 38 | } 39 | 40 | if (autoMappingInjector != null) { 41 | autoMappingInjector.inject(target); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/hannesdorfmann/fragmentargs/FragmentArgsInjector.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs; 2 | 3 | /** 4 | * Simple interface for the injector. This a class implementing this interface will be generated in 5 | * the processor 6 | * 7 | * @author Hannes Dorfmann 8 | */ 9 | public interface FragmentArgsInjector { 10 | 11 | public void inject(Object target); 12 | } 13 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/hannesdorfmann/fragmentargs/annotation/Arg.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.annotation; 2 | 3 | import com.hannesdorfmann.fragmentargs.bundler.ArgsBundler; 4 | import com.hannesdorfmann.fragmentargs.bundler.NoneArgsBundler; 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * Annotated every field that should be a fragment argument with this annotation 13 | * 14 | * @author Hannes Dorfmann 15 | */ 16 | @Target(ElementType.FIELD) 17 | @Retention(RetentionPolicy.CLASS) 18 | @Documented 19 | public @interface Arg { 20 | 21 | /** 22 | * Specifies if the argument is required (default) or not 23 | * 24 | * @return true if required, false otherwise 25 | */ 26 | boolean required() default true; 27 | 28 | /** 29 | * Key in the arguments bundle, by default uses the field name, minus the "m" prefix. 30 | */ 31 | String key() default ""; 32 | 33 | /** 34 | * You can specify the {@link ArgsBundler} class that should be used to put the annotated Arg into 35 | * the bundle and read from it. 36 | * 37 | * @return The Args Bundler class 38 | * @since 2.1 39 | */ 40 | Class bundler() default NoneArgsBundler.class; 41 | } 42 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/hannesdorfmann/fragmentargs/annotation/FragmentWithArgs.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Use this annotation to mark a Fragments that contains {@link Arg} annotation. This annotation is 11 | * required to run Annotation processing. 12 | * 13 | * @author Hannes Dorfmann 14 | * @since 3.0 15 | */ 16 | @Target(ElementType.TYPE) 17 | @Retention(RetentionPolicy.CLASS) 18 | @Documented 19 | public @interface FragmentWithArgs { 20 | 21 | /** 22 | * Is inheritance hierarchy scanning enabled? default value = true. Specifies if all @{@link Arg} 23 | * annotations of all super classes (checks the complete inheritance hierarchy) should be included 24 | * in the fragment. The default value is true and you don't have to specify that for each 25 | * fragment 26 | */ 27 | boolean inherited() default true; 28 | } 29 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/hannesdorfmann/fragmentargs/bundler/ArgsBundler.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.bundler; 2 | 3 | import android.os.Bundle; 4 | 5 | /** 6 | * With this class you can provide your own serialization and deserialization implementation to put 7 | * something into the bundle that is used by 8 | * 9 | * @author Hannes Dorfmann 10 | * @since 2.1 11 | */ 12 | public interface ArgsBundler { 13 | 14 | /** 15 | * Put (save) a value into the bundle. 16 | * 17 | * @param key The key you have to use as the key for the bundle to save the value 18 | * @param value The value you have to save into the bundle (for the given key) 19 | * @param bundle The Bundle to save key / value. It's not null. 20 | */ 21 | public void put(String key, T value, Bundle bundle); 22 | 23 | /** 24 | * Get a value from the bundle 25 | * 26 | * @param key The key for the value 27 | * @param bundle The Bundle where the value is saved in 28 | * @return The value retrieved from the Bundle with the given key 29 | */ 30 | public V get(String key, Bundle bundle); 31 | 32 | 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/hannesdorfmann/fragmentargs/bundler/CastedArrayListArgsBundler.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.bundler; 2 | 3 | import android.os.Bundle; 4 | import android.os.Parcelable; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * This {@link ArgsBundler} takes a java.util.List and casts it to an ArrayList. So it assumes that 10 | * the List is instance of ArrayList. 11 | *

12 | * With this ArgsBundler you can annotate fields of type java.util.List like that 13 | * {@code @Arg(bundler = CastedArrayListArgsBundler.class) List foos>} 14 | *

15 | * 16 | * @author Hannes Dorfmann 17 | * @since 2.1 18 | */ 19 | public class CastedArrayListArgsBundler implements ArgsBundler> { 20 | 21 | @Override public void put(String key, List value, Bundle bundle) { 22 | if (!(value instanceof ArrayList)) { 23 | throw new ClassCastException( 24 | "CastedArrayListArgsBundler assumes that the List is instance of ArrayList, but it's instance of " 25 | + value.getClass().getCanonicalName()); 26 | } 27 | 28 | bundle.putParcelableArrayList(key, (ArrayList) value); 29 | } 30 | 31 | @Override public > T get(String key, Bundle bundle) { 32 | return (T) bundle.getParcelableArrayList(key); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /annotation/src/main/java/com/hannesdorfmann/fragmentargs/bundler/NoneArgsBundler.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.bundler; 2 | 3 | import android.os.Bundle; 4 | import com.hannesdorfmann.fragmentargs.annotation.Arg; 5 | 6 | 7 | /** 8 | * This class is just representing that no {@link ArgsBundler} should be used. 9 | * The {@link Arg#bundler()} annotation uses this class to specify that no ArgsBundler should be 10 | * used and only the build in types should be used. 11 | * 12 | * @author Hannes Dorfmann 13 | * @since 2.1 14 | */ 15 | public final class NoneArgsBundler implements ArgsBundler { 16 | 17 | private NoneArgsBundler() { 18 | } 19 | 20 | @Override public void put(String key, Object value, Bundle bundle) { 21 | } 22 | 23 | @Override public Object get(String key, Bundle bundle) { 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bundler-parceler/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | fragmentargs-parent 8 | com.hannesdorfmann.fragmentargs 9 | 4.0.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | 4.0.0 13 | 14 | bundler-parceler 15 | 16 | 17 | 18 | 19 | ${project.groupId} 20 | annotation 21 | ${project.version} 22 | provided 23 | 24 | 25 | 26 | com.google.android 27 | android 28 | provided 29 | 30 | 31 | 32 | org.parceler 33 | parceler-api 34 | 1.0.0 35 | provided 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /bundler-parceler/src/main/java/com/hannesdorfmann/fragmentargs/bundler/ParcelerArgsBundler.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.bundler; 2 | 3 | import android.os.Bundle; 4 | import org.parceler.Parcels; 5 | 6 | /** 7 | * A {@link ArgsBundler} implementation for Parceler 8 | * 9 | * @author Hannes Dorfmann 10 | * @since 2.1 11 | */ 12 | public class ParcelerArgsBundler implements ArgsBundler { 13 | 14 | @Override public void put(String key, Object value, Bundle bundle) { 15 | bundle.putParcelable(key, Parcels.wrap(value)); 16 | } 17 | 18 | @Override public V get(String key, Bundle bundle) { 19 | return Parcels.unwrap(bundle.getParcelable(key)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /github-upload.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Die if something goes wrong. 4 | def die(msg); puts(msg); exit!(1); end 5 | 6 | # First thing we do is check the ruby version. This script requires 1.9, die 7 | # if that's not the case. 8 | die("This script requires ruby 1.9") unless RUBY_VERSION =~ /^1.9/ 9 | 10 | 11 | require 'json' 12 | require 'net/https' 13 | require 'pathname' 14 | require 'optparse' 15 | 16 | 17 | 18 | # Extensions 19 | # ---------- 20 | 21 | # We extend Pathname a bit to get the content type. 22 | class Pathname 23 | def type 24 | if $options[:mime_type] then 25 | $options[:mime_type] 26 | else 27 | flags = RUBY_PLATFORM =~ /darwin/ ? 'Ib' : 'ib' 28 | `file -#{flags} #{realpath}`.chomp.gsub(/;.*/,'') 29 | end 30 | end 31 | end 32 | 33 | 34 | 35 | # Helpers 36 | # ------- 37 | 38 | def get_http_request(uri, token, request, params = "") 39 | http = Net::HTTP.new(uri.host, uri.port) 40 | http.use_ssl = true 41 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE if $options[:skip_ssl_verification] 42 | request['Authorization'] = "token #{token}" if token 43 | return http.request(request, params) 44 | end 45 | 46 | def get(url, token) 47 | uri = URI.parse(url) 48 | req = Net::HTTP::Get.new(uri.path) 49 | return get_http_request(uri, token, req) 50 | end 51 | 52 | # Do a post to the given url, with the payload and optional basic auth. 53 | def post(url, token, params, headers) 54 | uri = URI.parse(url) 55 | req = Net::HTTP::Post.new(uri.path, headers) 56 | return get_http_request(uri, token, req, params) 57 | end 58 | 59 | def delete(url, token) 60 | uri = URI.parse(url) 61 | req = Net::HTTP::Delete.new(uri.path) 62 | return get_http_request(uri, token, req) 63 | end 64 | 65 | def urlencode(str) 66 | str.gsub(/[^a-zA-Z0-9_\.\-]/n) {|s| sprintf('%%%02x', s[0].to_i) } 67 | end 68 | 69 | # Yep, ruby net/http doesn't support multipart. Write our own multipart generator. 70 | # The order of the params is important, the file needs to go as last! 71 | def build_multipart_content(params) 72 | parts, boundary = [], "#{rand(1000000)}-we-are-all-doomed-#{rand(1000000)}" 73 | 74 | params.each do |name, value| 75 | data = [] 76 | if value.is_a?(Pathname) then 77 | data << "Content-Disposition: form-data; name=\"#{urlencode(name.to_s)}\"; filename=\"#{value.basename}\"" 78 | data << "Content-Type: #{value.type}" 79 | data << "Content-Length: #{value.size}" 80 | data << "Content-Transfer-Encoding: binary" 81 | data << "" 82 | data << value.binread 83 | else 84 | data << "Content-Disposition: form-data; name=\"#{urlencode(name.to_s)}\"" 85 | data << "" 86 | data << value 87 | end 88 | 89 | parts << data.join("\r\n") + "\r\n" 90 | end 91 | 92 | [ "--#{boundary}\r\n" + parts.join("--#{boundary}\r\n") + "--#{boundary}--", { 93 | "Content-Type" => "multipart/form-data; boundary=#{boundary}" 94 | }] 95 | end 96 | 97 | 98 | # Parse command line options using OptionParser 99 | # ----------------------- 100 | 101 | $options = {} 102 | 103 | OptionParser.new do |opts| 104 | 105 | opts.banner = "Usage: github-upload.rb [] [options]" 106 | 107 | opts.on("-d", "--description [DESCRIPTION]", 108 | "Add a description to the uploaded file.") do |arg_description| 109 | $options[:file_description] = arg_description 110 | end 111 | 112 | opts.on("-n", "--name [NAME]", 113 | "New name of the uploaded file.") do |arg_name| 114 | $options[:file_name] = arg_name 115 | end 116 | 117 | opts.on("-f", "--force", 118 | "If a file with that name already exists on the server, replace it with this one.") do 119 | $options[:force_upload] = true 120 | end 121 | 122 | opts.on("-t", "--token [TOKEN]", 123 | "Manually specify a GitHub API token. Useful if you want to temporarily upload a file under a different GitHub account.") do |arg_token| 124 | $options[:token] = arg_token 125 | end 126 | 127 | opts.on("--skip-ssl-verification", 128 | "Skip SSL Verification in the HTTP Request.") do 129 | $options[:skip_ssl_verification] = true 130 | end 131 | 132 | opts.on("-m", "--mime-type [TYPE]", 133 | "Manually specify mime-type instead autodetection.") do |arg_type| 134 | $options[:mime_type] = arg_type 135 | end 136 | 137 | opts.on("-h", "--help", 138 | "Show this message") do 139 | puts opts 140 | exit 141 | end 142 | 143 | end.parse! 144 | 145 | 146 | 147 | # Configuration and setup 148 | # ----------------------- 149 | 150 | # The file we want to upload, and repo where to upload it to. 151 | die("Please specify a file to upload.") if ARGV.length < 1 152 | file = Pathname.new(ARGV[0]) 153 | repo = ARGV[1] || `git config --get remote.origin.url`.match(/github.com[:\/](.+?)\.git/)[1] 154 | 155 | file_name = $options[:file_name] || file.basename.to_s 156 | file_description = $options[:file_description] || "" 157 | 158 | 159 | # The actual, hard work 160 | # --------------------- 161 | 162 | # Get Oauth token for this script. 163 | $options[:token] = `git config --get github.upload-script-token`.chomp unless $options[:token] 164 | 165 | 166 | if $options[:force_upload] 167 | 168 | # Make sure the file doesn't already exist 169 | res = get("https://api.github.com/repos/#{repo}/downloads", $options[:token]) 170 | info = JSON.parse(res.body) 171 | info.each do |remote_file| 172 | remote_file_name = remote_file["name"].to_s 173 | if remote_file_name == file_name then 174 | # Delete already existing files 175 | puts "Deleting existing file '#{remote_file_name}'" 176 | remote_file_id = remote_file["id"].to_s 177 | res = delete("https://api.github.com/repos/#{repo}/downloads/#{remote_file_id}", $options[:token]) 178 | end 179 | end 180 | 181 | end 182 | 183 | 184 | # Register the download at github. 185 | res = post("https://api.github.com/repos/#{repo}/downloads", $options[:token], { 186 | 'name' => file_name, 'size' => file.size.to_s, 187 | 'description' => file_description, 188 | 'content_type' => file.type.gsub(/;.*/, '') 189 | }.to_json, {}) 190 | 191 | die("File already exists named '#{file_name}'.") if res.class == Net::HTTPClientError 192 | die("GitHub doesn't want us to upload the file.") unless res.class == Net::HTTPCreated 193 | 194 | # The URL where people can download the file. At the end of the script, we 195 | # print it out for convenience. 196 | html_url = JSON.parse(res.body)['html_url'] 197 | 198 | # Parse the body and use the info to upload the file to S3. 199 | info = JSON.parse(res.body) 200 | res = post(info['s3_url'], nil, *build_multipart_content({ 201 | 'key' => info['path'], 'acl' => info['acl'], 'success_action_status' => 201, 202 | 'Filename' => info['name'], 'AWSAccessKeyId' => info['accesskeyid'], 203 | 'Policy' => info['policy'], 'signature' => info['signature'], 204 | 'Content-Type' => info['mime_type'], 'file' => file 205 | })) 206 | 207 | die("S3 is mean to us.") unless res.class == Net::HTTPCreated 208 | 209 | 210 | # Print the URL to the file to stdout. 211 | puts html_url -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.sonatype.oss 9 | oss-parent 10 | 7 11 | 12 | 13 | com.hannesdorfmann.fragmentargs 14 | fragmentargs-parent 15 | pom 16 | 4.0.0-SNAPSHOT 17 | 18 | annotation 19 | processor 20 | bundler-parceler 21 | 22 | 23 | 24 | fragmentargs-parent 25 | Generates Android Fragment Arguments 26 | https://github.com/sockeqwe/fragmentargs 27 | 28 | 29 | 30 | UTF-8 31 | 32 | 33 | 1.6 34 | 4.1.1.4 35 | 36 | 37 | 4.10 38 | 0.5 39 | 0.13 40 | 41 | 42 | 43 | 44 | 45 | ossrh 46 | https://oss.sonatype.org/content/repositories/snapshots 47 | 48 | 49 | ossrh 50 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 51 | 52 | 53 | 54 | 55 | 56 | GitHub Issues 57 | http://github.com/sockeqwe/fragmentargs/issues 58 | 59 | 60 | 61 | 62 | Apache 2.0 63 | http://www.apache.org/licenses/LICENSE-2.0.txt 64 | 65 | 66 | 67 | 68 | Hannes Dorfmann 69 | http://www.hannesdorfmann.com 70 | 71 | 72 | 73 | 74 | 75 | 76 | com.google.android 77 | android 78 | ${android.version} 79 | 80 | 81 | 82 | junit 83 | junit 84 | ${junit.version} 85 | 86 | 87 | com.google.testing.compile 88 | compile-testing 89 | ${compile-testing.version} 90 | 91 | 92 | org.truth0 93 | truth 94 | ${truth.version} 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-invoker-plugin 106 | 1.7 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-compiler-plugin 112 | 3.1 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-compiler-plugin 122 | 123 | ${java.version} 124 | ${java.version} 125 | -Xlint:all 126 | true 127 | true 128 | 129 | 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-javadoc-plugin 135 | 136 | 137 | attach-javadocs 138 | 139 | jar 140 | 141 | 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-source-plugin 148 | 149 | 150 | attach-sources 151 | 152 | jar 153 | 154 | 155 | 156 | 157 | 158 | 175 | 176 | 177 | org.apache.maven.plugins 178 | maven-checkstyle-plugin 179 | 2.10 180 | 181 | false 182 | true 183 | checkstyle.xml 184 | 185 | 186 | 187 | compile 188 | 189 | checkstyle 190 | 191 | 192 | 193 | 194 | 195 | 196 | 224 | 225 | org.codehaus.mojo 226 | versions-maven-plugin 227 | 2.1 228 | 229 | 230 | 231 | 232 | 276 | 277 | 278 | 279 | sonatype.oss.snapshots 280 | Sonatype OSS Snapshot Repository 281 | http://oss.sonatype.org/content/repositories/snapshots 282 | 283 | false 284 | 285 | 286 | true 287 | 288 | 289 | 290 | 291 | 292 | 293 | sonatype.oss.snapshots 294 | Sonatype OSS Snapshot Repository 295 | http://oss.sonatype.org/content/repositories/snapshots 296 | 297 | false 298 | 299 | 300 | true 301 | 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /processor/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | fragmentargs-parent 7 | com.hannesdorfmann.fragmentargs 8 | 4.0.0-SNAPSHOT 9 | ../pom.xml 10 | 11 | 4.0.0 12 | 13 | processor 14 | 15 | 16 | fragargs Processor 17 | 18 | Tool to generate Android Fragment processor 19 | 20 | 21 | 22 | 23 | 24 | ${project.groupId} 25 | annotation 26 | ${project.version} 27 | 28 | 29 | junit 30 | junit 31 | test 32 | 33 | 34 | org.mockito 35 | mockito-all 36 | 1.9.5 37 | test 38 | 39 | 40 | com.google.testing.compile 41 | compile-testing 42 | 0.9 43 | test 44 | 45 | 46 | com.google.truth 47 | truth 48 | 0.28 49 | test 50 | 51 | 52 | com.google.android 53 | android 54 | test 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.apache.maven.plugins 63 | maven-compiler-plugin 64 | 65 | 66 | default-compile 67 | 68 | compile 69 | 70 | 71 | -proc:none 72 | 73 | 74 | 75 | default-test-compile 76 | 77 | testCompile 78 | 79 | 80 | 81 | 82 | com.hannesdorfmann.fragmentargs.processor.ArgProcessor 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-invoker-plugin 93 | 94 | true 95 | ${project.build.directory}/it 96 | 97 | */pom.xml 98 | 99 | ${project.build.directory}/local-repo 100 | verify 101 | 102 | ${project.version} 103 | ${project.groupId} 104 | 105 | 106 | 107 | 108 | integration-test 109 | 110 | install 111 | run 112 | 113 | 114 | 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-javadoc-plugin 120 | 121 | 122 | attach-javadocs 123 | 124 | jar 125 | 126 | 127 | 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-source-plugin 133 | 134 | 135 | attach-sources 136 | 137 | jar 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /processor/src/main/java/com/hannesdorfmann/fragmentargs/processor/AnnotatedFragment.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import com.hannesdorfmann.fragmentargs.annotation.Arg; 4 | import java.util.HashMap; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.TreeSet; 10 | import javax.lang.model.element.Element; 11 | import javax.lang.model.element.ElementKind; 12 | import javax.lang.model.element.ExecutableElement; 13 | import javax.lang.model.element.TypeElement; 14 | import javax.lang.model.element.VariableElement; 15 | 16 | /** 17 | * Simple data holder class for fragment args of a certain fragment 18 | * 19 | * @author Hannes Dorfmann 20 | */ 21 | public class AnnotatedFragment { 22 | 23 | private Set requiredFields = new TreeSet(); 24 | private Set optional = new TreeSet(); 25 | private Map bundleKeyMap = 26 | new HashMap(); 27 | private TypeElement classElement; 28 | 29 | // qualified Bundler class is KEY, Varibale / Field name = VALUE 30 | private Map bundlerVariableMap = new HashMap(); 31 | private int bundlerCounter = 0; 32 | 33 | // Setter methods will be used 34 | private Map setterMethods = new HashMap(); 35 | 36 | public AnnotatedFragment(TypeElement classElement) { 37 | this.classElement = classElement; 38 | } 39 | 40 | public TypeElement getClassElement() { 41 | return classElement; 42 | } 43 | 44 | /** 45 | * Checks if a field (with the given name) is already in this class 46 | */ 47 | public boolean containsField(ArgumentAnnotatedField field) { 48 | return requiredFields.contains(field) || optional.contains(field); 49 | } 50 | 51 | /** 52 | * Checks if a key for a bundle has already been used 53 | */ 54 | public ArgumentAnnotatedField containsBundleKey(ArgumentAnnotatedField field) { 55 | return bundleKeyMap.get(field.getKey()); 56 | } 57 | 58 | private void checkAndSetCustomBundler(ArgumentAnnotatedField field) { 59 | 60 | if (field.hasCustomBundler()) { 61 | String bundlerClass = field.getBundlerClass(); 62 | String varName = bundlerVariableMap.get(bundlerClass); 63 | if (varName == null) { 64 | varName = "bundler" + (++bundlerCounter); 65 | bundlerVariableMap.put(bundlerClass, varName); 66 | } 67 | 68 | field.setBundlerFieldName(varName); 69 | } 70 | } 71 | 72 | /** 73 | * Adds an field as required 74 | */ 75 | public void addRequired(ArgumentAnnotatedField field) { 76 | bundleKeyMap.put(field.getKey(), field); 77 | requiredFields.add(field); 78 | checkAndSetCustomBundler(field); 79 | } 80 | 81 | /** 82 | * Adds an field as optional 83 | */ 84 | public void addOptional(ArgumentAnnotatedField field) { 85 | bundleKeyMap.put(field.getKey(), field); 86 | optional.add(field); 87 | 88 | checkAndSetCustomBundler(field); 89 | } 90 | 91 | public Set getRequiredFields() { 92 | return requiredFields; 93 | } 94 | 95 | public Set getOptionalFields() { 96 | return optional; 97 | } 98 | 99 | @Override 100 | public boolean equals(Object o) { 101 | if (this == o) return true; 102 | if (!(o instanceof AnnotatedFragment)) return false; 103 | 104 | AnnotatedFragment fragment = (AnnotatedFragment) o; 105 | 106 | if (!getQualifiedName().equals(fragment.getQualifiedName())) return false; 107 | 108 | return true; 109 | } 110 | 111 | @Override 112 | public int hashCode() { 113 | return getQualifiedName().hashCode(); 114 | } 115 | 116 | public String getQualifiedName() { 117 | return classElement.getQualifiedName().toString(); 118 | } 119 | 120 | public String getSimpleName() { 121 | return classElement.getSimpleName().toString(); 122 | } 123 | 124 | /* 125 | * Returns the Builder name 126 | * e.g. LoginFragmentBuilder or LoginActivity$$LoginFragment 127 | */ 128 | public String getBuilderName() { 129 | String builderName = getSimpleName() + "Builder"; 130 | 131 | if(isInnerClass()) { 132 | return classElement.getEnclosingElement().getSimpleName() + "$$" + builderName; 133 | 134 | } else { 135 | return builderName; 136 | } 137 | } 138 | 139 | /** 140 | * Returns the qualified Builder name 141 | * e.g. com.hannesdorfman.package.LoginFragmentBuilder or com.hannesdorfman.package.LoginActivity$$LoginFragment 142 | */ 143 | public String getQualifiedBuilderName() { 144 | String qualifiedBuilderName = getQualifiedName() + "Builder"; 145 | 146 | if(isInnerClass()) { 147 | return qualifiedBuilderName 148 | .replace("." + getSimpleName() + "Builder", "$$" + getSimpleName() + "Builder"); 149 | 150 | } else { 151 | return qualifiedBuilderName; 152 | } 153 | } 154 | 155 | public boolean isInnerClass() { 156 | return classElement.getEnclosingElement().getKind() == ElementKind.CLASS; 157 | } 158 | 159 | public Set getAll() { 160 | Set all = new HashSet(getRequiredFields()); 161 | all.addAll(getOptionalFields()); 162 | return all; 163 | } 164 | 165 | public Map getBundlerVariableMap() { 166 | return bundlerVariableMap; 167 | } 168 | 169 | /** 170 | * Checks if the given element is a valid setter method and add it to the internal setter 171 | * 172 | * @param classMember Could be everything except an field 173 | */ 174 | public void checkAndAddSetterMethod(Element classMember) { 175 | 176 | if (classMember.getKind() == ElementKind.METHOD) { 177 | ExecutableElement methodElement = (ExecutableElement) classMember; 178 | String methodName = methodElement.getSimpleName().toString(); 179 | if (methodName.startsWith("set")) { 180 | ExecutableElement existingSetter = setterMethods.get(methodName); 181 | if (existingSetter != null) { 182 | // Check for better visibility 183 | if (ModifierUtils.compareModifierVisibility(methodElement, existingSetter) == -1) { 184 | // this method has better visibility so use this one 185 | setterMethods.put(methodName, methodElement); 186 | } 187 | } else { 188 | setterMethods.put(methodName, methodElement); 189 | } 190 | } 191 | } 192 | 193 | } 194 | 195 | /** 196 | * Searches for a setter and returns the setter method 197 | * 198 | * @param field the {@link ArgumentAnnotatedField} 199 | * @return the setter method 200 | * @throws ProcessingException If no setter method has been found 201 | */ 202 | public ExecutableElement findSetterForField(ArgumentAnnotatedField field) throws ProcessingException { 203 | 204 | String fieldName = field.getVariableName(); 205 | StringBuilder builder = new StringBuilder("set"); 206 | if (fieldName.length() == 1) { 207 | builder.append(fieldName.toUpperCase()); 208 | } else { 209 | builder.append(Character.toUpperCase(fieldName.charAt(0))); 210 | builder.append(fieldName.substring(1)); 211 | } 212 | 213 | String methodName = builder.toString(); 214 | ExecutableElement setterMethod = setterMethods.get(methodName); 215 | if (setterMethod != null && isSetterApplicable(field, setterMethod)) { 216 | return setterMethod; // setter method found 217 | } 218 | 219 | // Search for setter method with hungarian notion check 220 | if (field.getName().length() > 1 && field.getName().matches("m[A-Z].*")) { 221 | // m not in lower case 222 | String hungarianMethodName = "set" + field.getName(); 223 | setterMethod = setterMethods.get(hungarianMethodName); 224 | if (setterMethod != null && isSetterApplicable(field, setterMethod)) { 225 | return setterMethod; // setter method found 226 | } 227 | 228 | // M in upper case 229 | hungarianMethodName = "set" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1); 230 | setterMethod = setterMethods.get(hungarianMethodName); 231 | if (setterMethod != null && isSetterApplicable(field, setterMethod)) { 232 | return setterMethod; // setter method found 233 | } 234 | } 235 | 236 | // Kotlin special boolean character treatment 237 | // Fields prefixed with "is" are not accessible through "setIsFoo" but with "setFoo" 238 | if (field.getName().length() > 1 && field.getName().matches("is[A-Z].*")) { 239 | String setterName = "set" + field.getName().substring(2); 240 | setterMethod = setterMethods.get(setterName); 241 | if (setterMethod != null && isSetterApplicable(field, setterMethod)) { 242 | return setterMethod; // setter method found 243 | } 244 | } 245 | 246 | throw new ProcessingException(field.getElement(), "The @%s annotated field '%s' in class %s has " + 247 | "private visibility. Hence a corresponding non-private setter method must be provided " + 248 | "called '%s(%s)'. Unfortunately this is not the case. Please add a setter method for " + 249 | "this field!", Arg.class.getSimpleName(), field.getName(), getSimpleName(), methodName, 250 | field.getType()); 251 | 252 | } 253 | 254 | private boolean isSetterApplicable(ArgumentAnnotatedField field, ExecutableElement setterMethod) { 255 | 256 | List parameters = setterMethod.getParameters(); 257 | if (parameters == null || parameters.size() != 1) { 258 | return false; 259 | } 260 | 261 | VariableElement parameter = parameters.get(0); 262 | return parameter.asType().equals(field.getElement().asType()); 263 | 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /processor/src/main/java/com/hannesdorfmann/fragmentargs/processor/ArgProcessor.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import com.hannesdorfmann.fragmentargs.FragmentArgs; 4 | import com.hannesdorfmann.fragmentargs.FragmentArgsInjector; 5 | import com.hannesdorfmann.fragmentargs.annotation.Arg; 6 | import com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs; 7 | import com.hannesdorfmann.fragmentargs.bundler.ArgsBundler; 8 | import com.hannesdorfmann.fragmentargs.repacked.com.squareup.javawriter.JavaWriter; 9 | 10 | import java.io.IOException; 11 | import java.io.Serializable; 12 | import java.io.Writer; 13 | import java.lang.annotation.Annotation; 14 | import java.util.ArrayList; 15 | import java.util.Arrays; 16 | import java.util.EnumSet; 17 | import java.util.HashMap; 18 | import java.util.HashSet; 19 | import java.util.LinkedHashSet; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.Set; 23 | import javax.annotation.processing.AbstractProcessor; 24 | import javax.annotation.processing.Filer; 25 | import javax.annotation.processing.ProcessingEnvironment; 26 | import javax.annotation.processing.RoundEnvironment; 27 | import javax.lang.model.SourceVersion; 28 | import javax.lang.model.element.Element; 29 | import javax.lang.model.element.ElementKind; 30 | import javax.lang.model.element.ExecutableElement; 31 | import javax.lang.model.element.Modifier; 32 | import javax.lang.model.element.PackageElement; 33 | import javax.lang.model.element.TypeElement; 34 | import javax.lang.model.type.TypeKind; 35 | import javax.lang.model.type.TypeMirror; 36 | import javax.lang.model.util.Elements; 37 | import javax.lang.model.util.Types; 38 | import javax.tools.Diagnostic; 39 | import javax.tools.JavaFileObject; 40 | 41 | /** 42 | * This is the annotation processor for FragmentArgs 43 | * 44 | * @author Hannes Dorfmann 45 | */ 46 | public class ArgProcessor extends AbstractProcessor { 47 | 48 | private static final String CUSTOM_BUNDLER_BUNDLE_KEY = 49 | "com.hannesdorfmann.fragmentargs.custom.bundler.2312A478rand."; 50 | 51 | private static final Map ARGUMENT_TYPES = 52 | new HashMap(20); 53 | 54 | /** 55 | * Annotation Processor Option 56 | */ 57 | private static final String OPTION_IS_LIBRARY = "fragmentArgsLib"; 58 | 59 | /** 60 | * Should the builder be annotated with support annotations? 61 | */ 62 | private static final String OPTION_SUPPORT_ANNOTATIONS = "fragmentArgsSupportAnnotations"; 63 | 64 | /** 65 | * Pass a list of additional annotations to annotate the generated builder classes 66 | */ 67 | private static final String OPTION_ADDITIONAL_BUILDER_ANNOTATIONS = 68 | "fragmentArgsBuilderAnnotations"; 69 | 70 | /** 71 | * Enable/disable warning logs 72 | */ 73 | private static final String OPTION_LOG_WARNINGS = "fragmentArgsLogWarnings"; 74 | 75 | static { 76 | ARGUMENT_TYPES.put("java.lang.String", "String"); 77 | ARGUMENT_TYPES.put("int", "Int"); 78 | ARGUMENT_TYPES.put("java.lang.Integer", "Int"); 79 | ARGUMENT_TYPES.put("long", "Long"); 80 | ARGUMENT_TYPES.put("java.lang.Long", "Long"); 81 | ARGUMENT_TYPES.put("double", "Double"); 82 | ARGUMENT_TYPES.put("java.lang.Double", "Double"); 83 | ARGUMENT_TYPES.put("short", "Short"); 84 | ARGUMENT_TYPES.put("java.lang.Short", "Short"); 85 | ARGUMENT_TYPES.put("float", "Float"); 86 | ARGUMENT_TYPES.put("java.lang.Float", "Float"); 87 | ARGUMENT_TYPES.put("byte", "Byte"); 88 | ARGUMENT_TYPES.put("java.lang.Byte", "Byte"); 89 | ARGUMENT_TYPES.put("boolean", "Boolean"); 90 | ARGUMENT_TYPES.put("java.lang.Boolean", "Boolean"); 91 | ARGUMENT_TYPES.put("char", "Char"); 92 | ARGUMENT_TYPES.put("java.lang.Character", "Char"); 93 | ARGUMENT_TYPES.put("java.lang.CharSequence", "CharSequence"); 94 | ARGUMENT_TYPES.put("android.os.Bundle", "Bundle"); 95 | ARGUMENT_TYPES.put("android.os.Parcelable", "Parcelable"); 96 | } 97 | 98 | private Types typeUtils; 99 | private Filer filer; 100 | 101 | private TypeElement TYPE_FRAGMENT; 102 | private TypeElement TYPE_SUPPORT_FRAGMENT; 103 | private TypeElement TYPE_ANDROIDX_FRAGMENT; 104 | private boolean supportAnnotations = true; 105 | private boolean logWarnings = true; 106 | 107 | @Override 108 | public Set getSupportedAnnotationTypes() { 109 | Set supportTypes = new LinkedHashSet(); 110 | supportTypes.add(Arg.class.getCanonicalName()); 111 | supportTypes.add(FragmentWithArgs.class.getCanonicalName()); 112 | return supportTypes; 113 | } 114 | 115 | @Override 116 | public Set getSupportedOptions() { 117 | Set supportedOptions = new LinkedHashSet(); 118 | supportedOptions.add(OPTION_IS_LIBRARY); 119 | supportedOptions.add(OPTION_ADDITIONAL_BUILDER_ANNOTATIONS); 120 | supportedOptions.add(OPTION_SUPPORT_ANNOTATIONS); 121 | supportedOptions.add(OPTION_LOG_WARNINGS); 122 | return supportedOptions; 123 | } 124 | 125 | @Override 126 | public synchronized void init(ProcessingEnvironment env) { 127 | super.init(env); 128 | 129 | Elements elementUtils = env.getElementUtils(); 130 | typeUtils = env.getTypeUtils(); 131 | filer = env.getFiler(); 132 | 133 | TYPE_FRAGMENT = elementUtils.getTypeElement("android.app.Fragment"); 134 | TYPE_SUPPORT_FRAGMENT = 135 | elementUtils.getTypeElement("android.support.v4.app.Fragment"); 136 | TYPE_ANDROIDX_FRAGMENT = 137 | elementUtils.getTypeElement("androidx.fragment.app.Fragment"); 138 | } 139 | 140 | private String getOperation(ArgumentAnnotatedField arg) { 141 | String op = ARGUMENT_TYPES.get(arg.getRawType()); 142 | if (op != null) { 143 | if (arg.isArray()) { 144 | return op + "Array"; 145 | } else { 146 | return op; 147 | } 148 | } 149 | 150 | Elements elements = processingEnv.getElementUtils(); 151 | TypeMirror type = arg.getElement().asType(); 152 | Types types = processingEnv.getTypeUtils(); 153 | String[] arrayListTypes = new String[]{ 154 | String.class.getName(), Integer.class.getName(), CharSequence.class.getName() 155 | }; 156 | String[] arrayListOps = 157 | new String[]{"StringArrayList", "IntegerArrayList", "CharSequenceArrayList"}; 158 | for (int i = 0; i < arrayListTypes.length; i++) { 159 | TypeMirror tm = getArrayListType(arrayListTypes[i]); 160 | if (types.isAssignable(type, tm)) { 161 | return arrayListOps[i]; 162 | } 163 | } 164 | 165 | if (types.isAssignable(type, 166 | getWildcardType(ArrayList.class.getName(), "android.os.Parcelable"))) { 167 | return "ParcelableArrayList"; 168 | } 169 | TypeMirror sparseParcelableArray = 170 | getWildcardType("android.util.SparseArray", "android.os.Parcelable"); 171 | 172 | if (types.isAssignable(type, sparseParcelableArray)) { 173 | return "SparseParcelableArray"; 174 | } 175 | 176 | if (types.isAssignable(type, elements.getTypeElement("android.os.Parcelable").asType())) { 177 | return "Parcelable"; 178 | } 179 | 180 | if (types.isAssignable(type, elements.getTypeElement(Serializable.class.getName()).asType())) { 181 | return "Serializable"; 182 | } 183 | 184 | return null; 185 | } 186 | 187 | private TypeMirror getWildcardType(String type, String elementType) { 188 | TypeElement arrayList = processingEnv.getElementUtils().getTypeElement(type); 189 | TypeMirror elType = processingEnv.getElementUtils().getTypeElement(elementType).asType(); 190 | return processingEnv.getTypeUtils() 191 | .getDeclaredType(arrayList, processingEnv.getTypeUtils().getWildcardType(elType, null)); 192 | } 193 | 194 | private TypeMirror getArrayListType(String elementType) { 195 | TypeElement arrayList = processingEnv.getElementUtils().getTypeElement("java.util.ArrayList"); 196 | TypeMirror elType = processingEnv.getElementUtils().getTypeElement(elementType).asType(); 197 | return processingEnv.getTypeUtils().getDeclaredType(arrayList, elType); 198 | } 199 | 200 | private void writePutArguments(JavaWriter jw, String sourceVariable, String bundleVariable, 201 | ArgumentAnnotatedField arg) throws IOException, ProcessingException { 202 | 203 | boolean addNullCheck = !arg.isPrimitive() && !arg.isRequired(); 204 | 205 | jw.emitEmptyLine(); 206 | 207 | if (addNullCheck) { 208 | jw.beginControlFlow("if (%s != null)", sourceVariable); 209 | } 210 | 211 | if (arg.hasCustomBundler()) { 212 | jw.emitStatement("%s.putBoolean(\"%s\", true)", bundleVariable, 213 | CUSTOM_BUNDLER_BUNDLE_KEY + arg.getKey()); 214 | jw.emitStatement("%s.put(\"%s\", %s, %s)", arg.getBundlerFieldName(), arg.getKey(), 215 | sourceVariable, bundleVariable); 216 | } else { 217 | 218 | String op = getOperation(arg); 219 | 220 | if (op == null) { 221 | throw new ProcessingException(arg.getElement(), 222 | "Don't know how to put %s in a Bundle. This type is not supported by default. " 223 | + "However, you can specify your own %s implementation in @Arg( bundler = YourBundler.class)", 224 | arg.getElement().asType().toString(), ArgsBundler.class.getSimpleName()); 225 | } 226 | 227 | if ("Serializable".equals(op)) { 228 | warn(arg.getElement(), 229 | "%1$s will be stored as Serializable", 230 | arg.getName() 231 | ); 232 | } 233 | 234 | jw.emitStatement("%4$s.put%1$s(\"%2$s\", %3$s)", op, arg.getKey(), sourceVariable, 235 | bundleVariable); 236 | } 237 | 238 | if (addNullCheck) { 239 | jw.endControlFlow(); 240 | } 241 | } 242 | 243 | private void writePackage(JavaWriter jw, TypeElement type) throws IOException { 244 | PackageElement pkg = processingEnv.getElementUtils().getPackageOf(type); 245 | if (!pkg.isUnnamed()) { 246 | jw.emitPackage(pkg.getQualifiedName().toString()); 247 | } else { 248 | jw.emitPackage(""); 249 | } 250 | } 251 | 252 | /** 253 | * Scans for @Arg annotations in the class itself and all super classes (complete inheritance 254 | * hierarchy) 255 | */ 256 | private AnnotatedFragment collectArgumentsForTypeInclSuperClasses(TypeElement type) 257 | throws ProcessingException { 258 | 259 | AnnotatedFragment fragment = new AnnotatedFragment(type); 260 | TypeElement currentClass = type; 261 | do { 262 | 263 | for (Element e : currentClass.getEnclosedElements()) { 264 | if (e.getKind() != ElementKind.FIELD) { 265 | fragment.checkAndAddSetterMethod(e); 266 | continue; 267 | } 268 | 269 | // It's a field 270 | Arg annotation = null; 271 | if ((annotation = e.getAnnotation(Arg.class)) != null) { 272 | ArgumentAnnotatedField annotatedField = 273 | new ArgumentAnnotatedField(e, (TypeElement) e.getEnclosingElement(), annotation); 274 | addAnnotatedField(annotatedField, fragment, annotation); 275 | } 276 | } 277 | 278 | TypeMirror superClassType = currentClass.getSuperclass(); 279 | if (superClassType.getKind() == TypeKind.NONE) { 280 | // Basis class (java.lang.Object) reached, so exit 281 | currentClass = null; 282 | break; 283 | } else { 284 | currentClass = (TypeElement) typeUtils.asElement(superClassType); 285 | } 286 | } while (currentClass != null); 287 | 288 | return fragment; 289 | } 290 | 291 | /** 292 | * Generates an error String with detailed information about that a field with the same name is 293 | * already defined in a super class 294 | */ 295 | private String getErrorMessageDuplicatedField(AnnotatedFragment fragment, 296 | TypeElement problemClass, String fieldName) { 297 | 298 | String base = 299 | "A field with the name '%s' in class %s is already annotated with @%s in super class %s ! " 300 | + "Fields name must be unique within inheritance hierarchy."; 301 | 302 | // Assumption: The problemClass is already a super class of the real problem, 303 | // So determine the real problem by searching for the subclass that cause this problem 304 | TypeElement otherClass = null; 305 | for (ArgumentAnnotatedField otherField : fragment.getAll()) { 306 | if (otherField.getVariableName().equals(fieldName)) { 307 | otherClass = otherField.getClassElement(); 308 | break; 309 | } 310 | } 311 | 312 | if (otherClass != null) { 313 | // Check who is the super class 314 | TypeElement currentClass = otherClass; 315 | 316 | while (currentClass != null) { 317 | TypeMirror currentClassSuperclass = currentClass.getSuperclass(); 318 | if (currentClassSuperclass == null || currentClassSuperclass.getKind() == TypeKind.NONE) { 319 | // They are not super classes 320 | break; 321 | } 322 | 323 | if (currentClass.getQualifiedName() != null && currentClass.getQualifiedName() 324 | .toString() 325 | .equals(problemClass.getQualifiedName().toString())) { 326 | // The problem causing class is a super class, so we found the superclass 327 | // and the sub class that cause the problem 328 | return String.format(base, fieldName, otherClass.getQualifiedName().toString(), 329 | Arg.class.getSimpleName(), problemClass.getQualifiedName()); 330 | } 331 | 332 | currentClass = (TypeElement) typeUtils.asElement(currentClassSuperclass); 333 | } 334 | } 335 | 336 | // Since the previous check wasn't successfull we can assume: 337 | // The problemClass must be a sub class, so find the super class that contains the field 338 | TypeMirror superClass = problemClass.getSuperclass(); 339 | TypeElement superClassElement = null; 340 | 341 | if (superClass == null) { 342 | return String.format( 343 | "A field with the name '%s' in class %s is already annotated with @%s in a super class or sub class! " 344 | + "Fields name must be unique within inheritance hierarchy.", fieldName, 345 | problemClass.getQualifiedName().toString(), Arg.class.getSimpleName()); 346 | } 347 | 348 | boolean superClassFound = false; 349 | while (superClass != null 350 | && superClass.getKind() != TypeKind.NONE 351 | && (superClassElement = (TypeElement) typeUtils.asElement(superClass)) != null) { 352 | 353 | for (Element e : superClassElement.getEnclosedElements()) { 354 | if (e.getKind() == ElementKind.FIELD && e.getSimpleName() != null && 355 | e.getSimpleName().toString().equals(fieldName)) { 356 | superClassFound = true; 357 | break; 358 | } 359 | } 360 | 361 | if (superClassFound) { 362 | break; 363 | } 364 | superClass = superClassElement.getSuperclass(); 365 | } 366 | 367 | if (superClassElement == null) { 368 | // Should never be the case, however to ensure we return a error message without superclass 369 | return String.format( 370 | "A field with the name '%s' in class %s is already annotated with @%s in a " 371 | + "super class or sub class of %s ! " 372 | + "Fields name must be unique within inheritance hierarchy.", fieldName, 373 | problemClass.getQualifiedName().toString(), Arg.class.getSimpleName(), 374 | problemClass.getQualifiedName().toString()); 375 | } 376 | 377 | return String.format(base, fieldName, problemClass.getQualifiedName().toString(), 378 | Arg.class.getSimpleName(), superClassElement.getQualifiedName()); 379 | } 380 | 381 | /** 382 | * Checks if the annotated field can be added to the given fragment. Otherwise a error message 383 | * will be printed 384 | */ 385 | private void addAnnotatedField(ArgumentAnnotatedField annotatedField, AnnotatedFragment fragment, 386 | Arg annotation) throws ProcessingException { 387 | 388 | if (fragment.containsField(annotatedField)) { 389 | // A field already with the name is here 390 | throw new ProcessingException(annotatedField.getElement(), 391 | getErrorMessageDuplicatedField(fragment, annotatedField.getClassElement(), 392 | annotatedField.getVariableName())); 393 | } else if (fragment.containsBundleKey(annotatedField) != null) { 394 | // key for bundle is already in use 395 | ArgumentAnnotatedField otherField = fragment.containsBundleKey(annotatedField); 396 | throw new ProcessingException(annotatedField.getElement(), 397 | "The bundle key '%s' for field %s in %s is already used by another " 398 | + "argument in %s (field name is '%s'). Bundle keys must be unique in inheritance hierarchy!", 399 | annotatedField.getKey(), annotatedField.getVariableName(), 400 | annotatedField.getClassElement().getQualifiedName().toString(), 401 | otherField.getClassElement().getQualifiedName().toString(), otherField.getVariableName()); 402 | } else { 403 | if (annotation.required()) { 404 | fragment.addRequired(annotatedField); 405 | } else { 406 | fragment.addOptional(annotatedField); 407 | } 408 | } 409 | } 410 | 411 | /** 412 | * Checks if inheritance hiererachy should be scanned for @Args annotations as well 413 | * 414 | * @param type The Fragment class 415 | * @return true if super type should be scanned as well, otherwise false; 416 | */ 417 | private boolean shouldScanSuperClassesFragmentArgs(TypeElement type) throws ProcessingException { 418 | 419 | boolean scanSuperClasses = true; 420 | 421 | FragmentWithArgs fragmentWithArgs = type.getAnnotation(FragmentWithArgs.class); 422 | if (fragmentWithArgs != null) { 423 | scanSuperClasses = fragmentWithArgs.inherited(); 424 | } 425 | 426 | return scanSuperClasses; // Default value 427 | } 428 | 429 | /** 430 | * Collects the fields that are annotated by the fragmentarg 431 | */ 432 | private AnnotatedFragment collectArgumentsForType(TypeElement type) throws ProcessingException { 433 | 434 | // incl. super classes 435 | if (shouldScanSuperClassesFragmentArgs(type)) { 436 | return collectArgumentsForTypeInclSuperClasses(type); 437 | } 438 | 439 | // Without super classes (inheritance) 440 | AnnotatedFragment fragment = new AnnotatedFragment(type); 441 | for (Element element : type.getEnclosedElements()) { 442 | if (element.getKind() == ElementKind.FIELD) { 443 | Arg annotation = element.getAnnotation(Arg.class); 444 | if (annotation != null) { 445 | ArgumentAnnotatedField field = new ArgumentAnnotatedField(element, type, annotation); 446 | addAnnotatedField(field, fragment, annotation); 447 | } 448 | } else { 449 | // check for setter 450 | fragment.checkAndAddSetterMethod(element); 451 | } 452 | } 453 | return fragment; 454 | } 455 | 456 | @Override 457 | public boolean process(Set type, RoundEnvironment env) { 458 | 459 | Types typeUtils = processingEnv.getTypeUtils(); 460 | Filer filer = processingEnv.getFiler(); 461 | 462 | // 463 | // Processor options 464 | // 465 | boolean isLibrary = false; 466 | String fragmentArgsLib = processingEnv.getOptions().get(OPTION_IS_LIBRARY); 467 | if (fragmentArgsLib != null && fragmentArgsLib.equalsIgnoreCase("true")) { 468 | isLibrary = true; 469 | } 470 | 471 | String supportAnnotationsStr = processingEnv.getOptions().get(OPTION_SUPPORT_ANNOTATIONS); 472 | if (supportAnnotationsStr != null && supportAnnotationsStr.equalsIgnoreCase("false")) { 473 | supportAnnotations = false; 474 | } 475 | 476 | String additionalBuilderAnnotations[] = {}; 477 | String builderAnnotationsStr = 478 | processingEnv.getOptions().get(OPTION_ADDITIONAL_BUILDER_ANNOTATIONS); 479 | if (builderAnnotationsStr != null && builderAnnotationsStr.length() > 0) { 480 | additionalBuilderAnnotations = builderAnnotationsStr.split(" "); // White space is delimiter 481 | } 482 | 483 | String fragmentArgsLogWarnings = processingEnv.getOptions().get(OPTION_LOG_WARNINGS); 484 | if(fragmentArgsLogWarnings != null && fragmentArgsLogWarnings.equalsIgnoreCase("false")) { 485 | logWarnings = false; 486 | } 487 | 488 | String nonNullAnnotationImport = ""; 489 | String nullableAnnotationImport = ""; 490 | 491 | if(supportAnnotations) { 492 | if (isClassAvailable("android.support.annotation.NonNull")) { 493 | nonNullAnnotationImport = "android.support.annotation.NonNull"; 494 | nullableAnnotationImport = "android.support.annotation.Nullable"; 495 | 496 | } else if (isClassAvailable("androidx.annotation.NonNull")) { 497 | nonNullAnnotationImport = "androidx.annotation.NonNull"; 498 | nullableAnnotationImport = "androidx.annotation.Nullable"; 499 | 500 | } else { 501 | supportAnnotations = false; 502 | warn(null, 503 | "Support annotations have been disabled because neither " + 504 | "'android.support.annotation.NonNull' nor " + 505 | "'androidx.annotation.NonNull' could be found during processing" 506 | ); 507 | } 508 | } 509 | 510 | List processingExceptions = new ArrayList(); 511 | 512 | JavaWriter jw = null; 513 | 514 | // REMEMBER: It's a SET! it uses .equals() .hashCode() to determine if element already in set 515 | Set fragmentClasses = new HashSet(); 516 | 517 | Element[] origHelper = null; 518 | 519 | // Search for @Arg fields 520 | for (Element element : env.getElementsAnnotatedWith(Arg.class)) { 521 | 522 | try { 523 | TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); 524 | 525 | // Check if its a fragment 526 | if (!isFragmentClass(enclosingElement)) { 527 | throw new ProcessingException(element, 528 | "@Arg can only be used on fragment fields (%s.%s)", 529 | enclosingElement.getQualifiedName(), element); 530 | } 531 | 532 | if (element.getModifiers().contains(Modifier.FINAL)) { 533 | throw new ProcessingException(element, 534 | "@Arg fields must not be final (%s.%s)", 535 | enclosingElement.getQualifiedName(), element); 536 | } 537 | 538 | if (element.getModifiers() 539 | .contains(Modifier.STATIC)) { 540 | throw new ProcessingException(element, 541 | "@Arg fields must not be static (%s.%s)", 542 | enclosingElement.getQualifiedName(), element); 543 | } 544 | 545 | // Skip abstract classes 546 | if (!enclosingElement.getModifiers().contains(Modifier.ABSTRACT)) { 547 | fragmentClasses.add(enclosingElement); 548 | } 549 | } catch (ProcessingException e) { 550 | processingExceptions.add(e); 551 | } 552 | } 553 | 554 | // Search for "just" @FragmentWithArgs 555 | for (Element element : env.getElementsAnnotatedWith(FragmentWithArgs.class)) { 556 | try { 557 | scanForAnnotatedFragmentClasses(env, FragmentWithArgs.class, fragmentClasses, element); 558 | } catch (ProcessingException e) { 559 | processingExceptions.add(e); 560 | } 561 | } 562 | 563 | // Store the key - value for the generated FragmentArtMap class 564 | Map autoMapping = new HashMap(); 565 | 566 | for (TypeElement fragmentClass : fragmentClasses) { 567 | 568 | JavaFileObject jfo = null; 569 | try { 570 | 571 | AnnotatedFragment fragment = collectArgumentsForType(fragmentClass); 572 | 573 | String builderName = fragment.getBuilderName(); 574 | 575 | List originating = new ArrayList(10); 576 | originating.add(fragmentClass); 577 | TypeMirror superClass = fragmentClass.getSuperclass(); 578 | while (superClass.getKind() != TypeKind.NONE) { 579 | TypeElement element = (TypeElement) typeUtils.asElement(superClass); 580 | if (element.getQualifiedName().toString().startsWith("android.")) { 581 | break; 582 | } 583 | originating.add(element); 584 | superClass = element.getSuperclass(); 585 | } 586 | 587 | String qualifiedFragmentName = fragment.getQualifiedName(); 588 | String qualifiedBuilderName = fragment.getQualifiedBuilderName(); 589 | 590 | Element[] orig = originating.toArray(new Element[originating.size()]); 591 | origHelper = orig; 592 | 593 | jfo = filer.createSourceFile(qualifiedBuilderName, orig); 594 | Writer writer = jfo.openWriter(); 595 | jw = new JavaWriter(writer); 596 | writePackage(jw, fragmentClass); 597 | jw.emitImports("android.os.Bundle"); 598 | if (supportAnnotations) { 599 | jw.emitImports(nonNullAnnotationImport); 600 | if (!fragment.getOptionalFields().isEmpty()) { 601 | jw.emitImports(nullableAnnotationImport); 602 | } 603 | } 604 | 605 | // for inner classes we need to add an import 606 | if(fragment.isInnerClass()) { 607 | jw.emitImports(fragment.getQualifiedName()); 608 | } 609 | 610 | jw.emitEmptyLine(); 611 | 612 | // Additional builder annotations 613 | for (String builderAnnotation : additionalBuilderAnnotations) { 614 | jw.emitAnnotation(builderAnnotation); 615 | } 616 | 617 | jw.beginType(builderName, "class", EnumSet.of(Modifier.PUBLIC, Modifier.FINAL)); 618 | 619 | if (!fragment.getBundlerVariableMap().isEmpty()) { 620 | jw.emitEmptyLine(); 621 | for (Map.Entry e : fragment.getBundlerVariableMap().entrySet()) { 622 | jw.emitField(e.getKey(), e.getValue(), 623 | EnumSet.of(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC), 624 | "new " + e.getKey() + "()"); 625 | } 626 | } 627 | jw.emitEmptyLine(); 628 | jw.emitField("Bundle", "mArguments", EnumSet.of(Modifier.PRIVATE, Modifier.FINAL), 629 | "new Bundle()"); 630 | jw.emitEmptyLine(); 631 | 632 | Set required = fragment.getRequiredFields(); 633 | 634 | String[] args = new String[required.size() * 2]; 635 | int index = 0; 636 | 637 | for (ArgumentAnnotatedField arg : required) { 638 | boolean annotate = supportAnnotations && !arg.isPrimitive(); 639 | args[index++] = annotate ? "@NonNull " + arg.getType() : arg.getType(); 640 | args[index++] = arg.getVariableName(); 641 | } 642 | jw.beginMethod(null, builderName, EnumSet.of(Modifier.PUBLIC), args); 643 | 644 | for (ArgumentAnnotatedField arg : required) { 645 | writePutArguments(jw, arg.getVariableName(), "mArguments", arg); 646 | } 647 | 648 | jw.endMethod(); 649 | 650 | if (!required.isEmpty()) { 651 | jw.emitEmptyLine(); 652 | writeNewFragmentWithRequiredMethod(builderName, fragmentClass, jw, args); 653 | } 654 | 655 | Set optionalArguments = fragment.getOptionalFields(); 656 | 657 | for (ArgumentAnnotatedField arg : optionalArguments) { 658 | writeBuilderMethod(builderName, jw, arg); 659 | } 660 | 661 | jw.emitEmptyLine(); 662 | writeBuildBundleMethod(jw); 663 | 664 | jw.emitEmptyLine(); 665 | writeInjectMethod(jw, fragmentClass, fragment); 666 | 667 | jw.emitEmptyLine(); 668 | writeBuildMethod(jw, fragmentClass); 669 | 670 | jw.endType(); 671 | 672 | autoMapping.put(qualifiedFragmentName, qualifiedBuilderName); 673 | } catch (IOException e) { 674 | processingExceptions.add( 675 | new ProcessingException(fragmentClass, "Unable to write builder for type %s: %s", 676 | fragmentClass, e.getMessage())); 677 | } catch (ProcessingException e) { 678 | processingExceptions.add(e); 679 | if (jfo != null) { 680 | jfo.delete(); 681 | } 682 | } finally { 683 | if (jw != null) { 684 | try { 685 | jw.close(); 686 | } catch (IOException e1) { 687 | processingExceptions.add(new ProcessingException(fragmentClass, 688 | "Unable to close javawriter while generating builder for type %s: %s", 689 | fragmentClass, e1.getMessage())); 690 | } 691 | } 692 | } 693 | } 694 | 695 | // Write the automapping class 696 | if (origHelper != null && !isLibrary) { 697 | try { 698 | writeAutoMapping(autoMapping, origHelper); 699 | } catch (ProcessingException e) { 700 | processingExceptions.add(e); 701 | } 702 | } 703 | 704 | // Print errors 705 | for (ProcessingException e : processingExceptions) { 706 | error(e); 707 | } 708 | 709 | return true; 710 | } 711 | 712 | /** 713 | * Write the buildBundle() method 714 | * 715 | * @param jw The javawriter 716 | * @throws IOException 717 | */ 718 | private void writeBuildBundleMethod(JavaWriter jw) throws IOException { 719 | if (supportAnnotations) jw.emitAnnotation("NonNull"); 720 | jw.beginMethod("Bundle", "buildBundle", EnumSet.of(Modifier.PUBLIC)); 721 | jw.emitStatement("return new Bundle(mArguments)"); 722 | jw.endMethod(); 723 | } 724 | 725 | /** 726 | * Scans a fragment for a given {@link FragmentWithArgs} annotation 727 | * 728 | * @param env The round environment 729 | * @param annotationClass The annotation (.class) to scan for 730 | * @param fragmentClasses The set of classes already scanned (containing annotations) 731 | * @throws ProcessingException 732 | */ 733 | private void scanForAnnotatedFragmentClasses(RoundEnvironment env, 734 | Class annotationClass, Set fragmentClasses, 735 | Element element) 736 | throws ProcessingException { 737 | 738 | if (element.getKind() != ElementKind.CLASS) { 739 | throw new ProcessingException(element, "%s can only be applied on Fragment classes", 740 | annotationClass.getSimpleName()); 741 | } 742 | 743 | TypeElement classElement = (TypeElement) element; 744 | 745 | // Check if its a fragment 746 | if (!isFragmentClass(element)) { 747 | throw new ProcessingException(element, 748 | "%s can only be used on fragments, but %s is not a subclass of fragment", 749 | annotationClass.getSimpleName(), classElement.getQualifiedName()); 750 | } 751 | 752 | // Skip abstract classes 753 | if (!classElement.getModifiers().contains(Modifier.ABSTRACT)) { 754 | fragmentClasses.add(classElement); 755 | } 756 | } 757 | 758 | /** 759 | * Checks if the given element is in a valid Fragment class 760 | */ 761 | private boolean isFragmentClass(Element classElement) { 762 | List fragmentTypeElements = Arrays.asList(TYPE_FRAGMENT, TYPE_SUPPORT_FRAGMENT, TYPE_ANDROIDX_FRAGMENT); 763 | 764 | for (TypeElement fragmentTypeElement : fragmentTypeElements) { 765 | if(fragmentTypeElement != null && typeUtils.isSubtype(classElement.asType(), 766 | fragmentTypeElement.asType())) { 767 | return true; 768 | } 769 | } 770 | 771 | return false; 772 | } 773 | 774 | private boolean isClassAvailable(String className) { 775 | try { 776 | Class.forName(className); 777 | return true; 778 | } catch (ClassNotFoundException e) { 779 | return false; 780 | } 781 | } 782 | 783 | /** 784 | * Key is the fully qualified fragment name, value is the fully qualified Builder class name 785 | */ 786 | 787 | private void writeAutoMapping(Map mapping, Element[] element) 788 | throws ProcessingException { 789 | 790 | try { 791 | JavaFileObject jfo = 792 | filer.createSourceFile(FragmentArgs.AUTO_MAPPING_QUALIFIED_CLASS, element); 793 | Writer writer = jfo.openWriter(); 794 | JavaWriter jw = new JavaWriter(writer); 795 | // Package 796 | jw.emitPackage(FragmentArgs.AUTO_MAPPING_PACKAGE); 797 | 798 | // Class 799 | jw.beginType(FragmentArgs.AUTO_MAPPING_CLASS_NAME, "class", 800 | EnumSet.of(Modifier.PUBLIC, Modifier.FINAL), null, 801 | FragmentArgsInjector.class.getCanonicalName()); 802 | 803 | jw.emitEmptyLine(); 804 | // The mapping Method 805 | jw.emitAnnotation("Override"); 806 | jw.beginMethod("void", "inject", EnumSet.of(Modifier.PUBLIC), "Object", "target"); 807 | 808 | jw.emitEmptyLine(); 809 | jw.emitStatement("Class targetClass = target.getClass()"); 810 | jw.emitStatement("String targetName = targetClass.getCanonicalName()"); 811 | // TODO should be targetClass.getName()? Inner anonymous class not possible? 812 | 813 | for (Map.Entry entry : mapping.entrySet()) { 814 | 815 | jw.emitEmptyLine(); 816 | jw.beginControlFlow("if ( %s.class.getName().equals(targetName) )", entry.getKey()); 817 | jw.emitStatement("%s.injectArguments( ( %s ) target)", entry.getValue(), entry.getKey()); 818 | jw.emitStatement("return"); 819 | jw.endControlFlow(); 820 | } 821 | 822 | // End Mapping method 823 | jw.endMethod(); 824 | 825 | jw.endType(); 826 | jw.close(); 827 | } catch (IOException e) { 828 | throw new ProcessingException(null, 829 | "Unable to write the automapping class for builder to fragment: %s: %s", 830 | FragmentArgs.AUTO_MAPPING_QUALIFIED_CLASS, e.getMessage()); 831 | } 832 | } 833 | 834 | private void writeNewFragmentWithRequiredMethod(String builder, TypeElement element, 835 | JavaWriter jw, String[] args) throws IOException { 836 | 837 | if (supportAnnotations) jw.emitAnnotation("NonNull"); 838 | jw.beginMethod(element.getQualifiedName().toString(), "new" + element.getSimpleName(), 839 | EnumSet.of(Modifier.STATIC, Modifier.PUBLIC), args); 840 | StringBuilder argNames = new StringBuilder(); 841 | for (int i = 1; i < args.length; i += 2) { 842 | argNames.append(args[i]); 843 | if (i < args.length - 1) { 844 | argNames.append(", "); 845 | } 846 | } 847 | jw.emitStatement("return new %1$s(%2$s).build()", builder, argNames); 848 | jw.endMethod(); 849 | } 850 | 851 | private void writeBuildMethod(JavaWriter jw, TypeElement element) throws IOException { 852 | if (supportAnnotations) { 853 | jw.emitAnnotation("NonNull"); 854 | } 855 | 856 | jw.beginMethod(element.getSimpleName().toString(), "build", EnumSet.of(Modifier.PUBLIC)); 857 | jw.emitStatement("%1$s fragment = new %1$s()", element.getSimpleName().toString()); 858 | jw.emitStatement("fragment.setArguments(mArguments)"); 859 | jw.emitStatement("return fragment"); 860 | jw.endMethod(); 861 | } 862 | 863 | private void writeInjectMethod(JavaWriter jw, TypeElement element, 864 | AnnotatedFragment fragment) throws IOException, ProcessingException { 865 | 866 | Set allArguments = fragment.getAll(); 867 | 868 | String fragmentType = supportAnnotations ? "@NonNull " + element.getSimpleName().toString() 869 | : element.getSimpleName().toString(); 870 | 871 | jw.beginMethod("void", "injectArguments", 872 | EnumSet.of(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC), 873 | fragmentType, "fragment"); 874 | 875 | 876 | jw.emitStatement("Bundle args = fragment.getArguments()"); 877 | 878 | // Check if bundle is null only if at least one required field 879 | jw.beginControlFlow("if (args == null)"); 880 | jw.emitStatement( 881 | "throw new IllegalStateException(\"No arguments set. Have you set up this Fragment with the corresponding FragmentArgs Builder? \")"); 882 | jw.endControlFlow(); 883 | 884 | 885 | int setterAssignmentHelperCounter = 0; 886 | for (ArgumentAnnotatedField field : allArguments) { 887 | jw.emitEmptyLine(); 888 | 889 | Set modifiers = field.getElement().getModifiers(); 890 | 891 | // Check if the given setter is available 892 | String setterMethod = null; 893 | 894 | // Private fields and non-public fields from a different package need a setter method 895 | boolean useSetter = modifiers.contains(Modifier.PRIVATE) 896 | || (!getPackage(fragment.getClassElement()).equals(getPackage(field.getElement())) && !modifiers.contains(Modifier.PUBLIC)); 897 | 898 | if (useSetter) { 899 | ExecutableElement setterMethodElement = fragment.findSetterForField(field); 900 | setterMethod = setterMethodElement.getSimpleName().toString(); 901 | } 902 | 903 | // Args Bundler 904 | if (field.hasCustomBundler()) { 905 | 906 | String setterAssignmentHelperStr = null; 907 | String assignmentStr; 908 | if (useSetter) { 909 | setterAssignmentHelperStr = field.getType() 910 | + " value" 911 | + setterAssignmentHelperCounter 912 | + " = %s.get(\"%s\", args)"; 913 | assignmentStr = "fragment.%s( value" + setterAssignmentHelperCounter + " )"; 914 | setterAssignmentHelperCounter++; 915 | } else { 916 | assignmentStr = "fragment.%s = %s.get(\"%s\", args)"; 917 | } 918 | 919 | // Required 920 | if (field.isRequired()) { 921 | jw.beginControlFlow("if (!args.containsKey(" + JavaWriter.stringLiteral( 922 | CUSTOM_BUNDLER_BUNDLE_KEY + field.getKey()) + "))"); 923 | jw.emitStatement("throw new IllegalStateException(\"required argument %1$s is not set\")", 924 | field.getKey()); 925 | jw.endControlFlow(); 926 | if (useSetter) { 927 | jw.emitStatement(setterAssignmentHelperStr, field.getBundlerFieldName(), 928 | field.getKey()); 929 | jw.emitStatement(assignmentStr, setterMethod); 930 | } else { 931 | jw.emitStatement(assignmentStr, field.getName(), 932 | field.getBundlerFieldName(), field.getKey()); 933 | } 934 | } else { 935 | // not required bundler 936 | jw.beginControlFlow("if (args.getBoolean(" + JavaWriter.stringLiteral( 937 | CUSTOM_BUNDLER_BUNDLE_KEY + field.getKey()) + "))"); 938 | 939 | if (useSetter) { 940 | jw.emitStatement(setterAssignmentHelperStr, field.getBundlerFieldName(), 941 | field.getKey()); 942 | jw.emitStatement(assignmentStr, setterMethod); 943 | } else { 944 | jw.emitStatement(assignmentStr, field.getName(), 945 | field.getBundlerFieldName(), field.getKey()); 946 | } 947 | 948 | jw.endControlFlow(); 949 | } 950 | } else { 951 | 952 | // Build in functions 953 | String op = getOperation(field); 954 | if (op == null) { 955 | throw new ProcessingException(element, 956 | "Can't write injector, the type is not supported by default. " 957 | + "However, You can provide your own implementation by providing an %s like this: @Arg( bundler = YourBundler.class )", 958 | ArgsBundler.class.getSimpleName()); 959 | } 960 | 961 | String cast = "Serializable".equals(op) ? "(" + field.getType() + ") " : ""; 962 | if (!field.isRequired()) { 963 | jw.beginControlFlow( 964 | "if (args != null && args.containsKey(" 965 | + JavaWriter.stringLiteral(field.getKey()) 966 | + "))"); 967 | } else { 968 | jw.beginControlFlow( 969 | "if (!args.containsKey(" + JavaWriter.stringLiteral(field.getKey()) + "))"); 970 | jw.emitStatement("throw new IllegalStateException(\"required argument %1$s is not set\")", 971 | field.getKey()); 972 | jw.endControlFlow(); 973 | } 974 | 975 | if (useSetter) { 976 | jw.emitStatement( 977 | "%1$s value" + setterAssignmentHelperCounter + " = %4$sargs.get%2$s(\"%3$s\")", 978 | field.getType(), op, 979 | field.getKey(), cast); 980 | jw.emitStatement("fragment.%1$s(value" + setterAssignmentHelperCounter + ")", 981 | setterMethod); 982 | setterAssignmentHelperCounter++; 983 | } else { 984 | jw.emitStatement("fragment.%1$s = %4$sargs.get%2$s(\"%3$s\")", field.getName(), op, 985 | field.getKey(), cast); 986 | } 987 | 988 | if (!field.isRequired()) { 989 | jw.endControlFlow(); 990 | } 991 | } 992 | } 993 | jw.endMethod(); 994 | } 995 | 996 | private void writeBuilderMethod(String type, JavaWriter writer, ArgumentAnnotatedField arg) 997 | throws IOException, ProcessingException { 998 | writer.emitEmptyLine(); 999 | boolean annotate = supportAnnotations && !arg.isPrimitive(); 1000 | 1001 | String typeStr; 1002 | if (annotate) { 1003 | if (arg.isRequired()) { 1004 | typeStr = "@NonNull " + arg.getType(); 1005 | } else { 1006 | typeStr = "@Nullable " + arg.getType(); 1007 | } 1008 | } else { 1009 | typeStr = arg.getType(); 1010 | } 1011 | 1012 | if (supportAnnotations) writer.emitAnnotation("NonNull"); 1013 | writer.beginMethod(type, arg.getVariableName(), EnumSet.of(Modifier.PUBLIC), 1014 | typeStr, arg.getVariableName()); 1015 | writePutArguments(writer, arg.getVariableName(), "mArguments", arg); 1016 | writer.emitStatement("return this"); 1017 | writer.endMethod(); 1018 | } 1019 | 1020 | private void error(ProcessingException e) { 1021 | String message = e.getMessage(); 1022 | if (e.getMessageArgs().length > 0) { 1023 | message = String.format(message, e.getMessageArgs()); 1024 | } 1025 | processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, e.getElement()); 1026 | } 1027 | 1028 | private void warn(Element element, String message, Object... args) { 1029 | if(logWarnings) { 1030 | if (args.length > 0) { 1031 | message = String.format(message, args); 1032 | } 1033 | processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message, element); 1034 | } 1035 | } 1036 | 1037 | private PackageElement getPackage(Element element) { 1038 | while (element.getKind() != ElementKind.PACKAGE) { 1039 | element = element.getEnclosingElement(); 1040 | } 1041 | return (PackageElement) element; 1042 | } 1043 | 1044 | @Override 1045 | public SourceVersion getSupportedSourceVersion() { 1046 | return SourceVersion.latestSupported(); 1047 | } 1048 | } 1049 | -------------------------------------------------------------------------------- /processor/src/main/java/com/hannesdorfmann/fragmentargs/processor/ArgumentAnnotatedField.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import com.hannesdorfmann.fragmentargs.annotation.Arg; 4 | import com.hannesdorfmann.fragmentargs.bundler.ArgsBundler; 5 | import com.hannesdorfmann.fragmentargs.bundler.NoneArgsBundler; 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.Modifier; 8 | import java.util.List; 9 | import javax.lang.model.element.Element; 10 | import javax.lang.model.element.ElementKind; 11 | import javax.lang.model.element.ExecutableElement; 12 | import javax.lang.model.element.TypeElement; 13 | import javax.lang.model.type.DeclaredType; 14 | import javax.lang.model.type.MirroredTypeException; 15 | import javax.lang.model.type.TypeKind; 16 | import javax.lang.model.type.TypeMirror; 17 | 18 | public class ArgumentAnnotatedField implements Comparable { 19 | 20 | private final String name; 21 | private final String key; 22 | private final String type; 23 | private final Element element; 24 | private final boolean required; 25 | private final TypeElement classElement; 26 | 27 | private String bundlerClass; 28 | private String bundlerFieldName; 29 | 30 | public ArgumentAnnotatedField(Element element, TypeElement classElement, Arg annotation) 31 | throws ProcessingException { 32 | 33 | this.name = element.getSimpleName().toString(); 34 | this.key = getKey(element, annotation); 35 | this.type = element.asType().toString(); 36 | this.element = element; 37 | this.required = annotation.required(); 38 | this.classElement = classElement; 39 | 40 | try { 41 | Class clazz = annotation.bundler(); 42 | bundlerClass = getFullQualifiedNameByClass(clazz); 43 | } catch (MirroredTypeException mte) { 44 | TypeMirror baggerClass = mte.getTypeMirror(); 45 | bundlerClass = getFullQualifiedNameByTypeMirror(baggerClass); 46 | } 47 | } 48 | 49 | public String getBundlerFieldName() { 50 | return bundlerFieldName; 51 | } 52 | 53 | public void setBundlerFieldName(String bundlerFieldName) { 54 | this.bundlerFieldName = bundlerFieldName; 55 | } 56 | 57 | private static String getKey(Element element, Arg annotation) { 58 | String field = element.getSimpleName().toString(); 59 | if (!"".equals(annotation.key())) { 60 | return annotation.key(); 61 | } 62 | return getVariableName(field); 63 | } 64 | 65 | private static boolean isRequired(Element element) { 66 | Arg annotation = element.getAnnotation(Arg.class); 67 | return annotation.required(); 68 | } 69 | 70 | private String getFullQualifiedNameByTypeMirror(TypeMirror baggerClass) 71 | throws ProcessingException { 72 | if (baggerClass == null) { 73 | throw new ProcessingException(element, "Could not get the ArgsBundler class"); 74 | } 75 | 76 | // If its the NoneArgsBundler, no future checks are required 77 | if (baggerClass.toString().equals(NoneArgsBundler.class.getCanonicalName())) { 78 | return NoneArgsBundler.class.getCanonicalName(); 79 | } 80 | 81 | if (baggerClass.getKind() != TypeKind.DECLARED) { 82 | throw new ProcessingException(element, "@ %s is not a class in %s ", 83 | ArgsBundler.class.getSimpleName(), element.getSimpleName()); 84 | } 85 | 86 | if (!isPublicClass((DeclaredType) baggerClass)) { 87 | throw new ProcessingException(element, 88 | "The %s must be a public class to be a valid ArgsBundler", baggerClass.toString()); 89 | } 90 | 91 | // Check if the bagger class has a default constructor 92 | if (!hasPublicEmptyConstructor((DeclaredType) baggerClass)) { 93 | throw new ProcessingException(element, 94 | "The %s must provide a public empty default constructor to be a valid ArgsBundler", 95 | baggerClass.toString()); 96 | } 97 | 98 | return baggerClass.toString(); 99 | } 100 | 101 | private String getFullQualifiedNameByClass(Class clazz) 102 | throws ProcessingException { 103 | 104 | // It's the none Args bundler, hence no future checks are needed 105 | if (clazz.equals(NoneArgsBundler.class)) { 106 | return NoneArgsBundler.class.getCanonicalName(); 107 | } 108 | 109 | // Check public 110 | if (!Modifier.isPublic(clazz.getModifiers())) { 111 | 112 | throw new ProcessingException(element, 113 | "The %s must be a public class to be a valid ArgsBundler", clazz.getCanonicalName()); 114 | } 115 | 116 | // Check constructors 117 | Constructor[] constructors = clazz.getConstructors(); 118 | 119 | boolean foundDefaultConstructor = false; 120 | for (Constructor c : constructors) { 121 | boolean isPublicConstructor = Modifier.isPublic(c.getModifiers()); 122 | Class[] pType = c.getParameterTypes(); 123 | 124 | if (pType.length == 0 && isPublicConstructor) { 125 | foundDefaultConstructor = true; 126 | break; 127 | } 128 | } 129 | 130 | if (!foundDefaultConstructor) { 131 | throw new ProcessingException(element, 132 | "The %s must provide a public empty default constructor", clazz.getCanonicalName()); 133 | } 134 | 135 | return clazz.getCanonicalName(); 136 | } 137 | 138 | /** 139 | * Checks if a class is public 140 | */ 141 | private boolean isPublicClass(DeclaredType type) { 142 | Element element = type.asElement(); 143 | 144 | return element.getModifiers().contains(javax.lang.model.element.Modifier.PUBLIC); 145 | } 146 | 147 | /** 148 | * Checks if an public empty constructor is available 149 | */ 150 | private boolean hasPublicEmptyConstructor(DeclaredType type) { 151 | Element element = type.asElement(); 152 | 153 | List containing = element.getEnclosedElements(); 154 | 155 | for (Element e : containing) { 156 | if (e.getKind() == ElementKind.CONSTRUCTOR) { 157 | ExecutableElement c = (ExecutableElement) e; 158 | 159 | if ((c.getParameters() == null || c.getParameters().isEmpty()) && c.getModifiers() 160 | .contains(javax.lang.model.element.Modifier.PUBLIC)) { 161 | return true; 162 | } 163 | } 164 | } 165 | 166 | return false; 167 | } 168 | 169 | /** 170 | * Get the full qualified name of the ArgsBundler class. 171 | * 172 | * @return null if no custom bundler has been set. Otherwise the fully qualified name of the 173 | * custom bundler class. 174 | */ 175 | public String getBundlerClass() { 176 | return bundlerClass.equals(NoneArgsBundler.class.getCanonicalName()) ? null : bundlerClass; 177 | } 178 | 179 | public boolean hasCustomBundler() { 180 | return getBundlerClass() != null; 181 | } 182 | 183 | public TypeElement getClassElement() { 184 | return classElement; 185 | } 186 | 187 | public String getVariableName() { 188 | return getVariableName(name); 189 | } 190 | 191 | public static String getVariableName(String name) { 192 | if (name.matches("^m[A-Z]{1}") || name.matches("^_[A-Za-z]{1}")) { 193 | return name.substring(1, 2).toLowerCase(); 194 | } else if (name.matches("m[A-Z]{1}.*") || name.matches("_[A-Za-z]{1}.*")) { 195 | return name.substring(1, 2).toLowerCase() + name.substring(2); 196 | } 197 | return name; 198 | } 199 | 200 | public String getKey() { 201 | return this.key; 202 | } 203 | 204 | public String getName() { 205 | return name; 206 | } 207 | 208 | public String getType() { 209 | return type; 210 | } 211 | 212 | public Element getElement() { 213 | return element; 214 | } 215 | 216 | public boolean isRequired() { 217 | return required; 218 | } 219 | 220 | @Override 221 | public boolean equals(Object o) { 222 | if (this == o) return true; 223 | if (!(o instanceof ArgumentAnnotatedField)) return false; 224 | 225 | ArgumentAnnotatedField that = (ArgumentAnnotatedField) o; 226 | 227 | if (!name.equals(that.name)) return false; 228 | 229 | return true; 230 | } 231 | 232 | @Override 233 | public int hashCode() { 234 | return name.hashCode(); 235 | } 236 | 237 | @Override 238 | public String toString() { 239 | return key + "/" + type; 240 | } 241 | 242 | public String getRawType() { 243 | if (isArray()) { 244 | return type.substring(0, type.length() - 2); 245 | } 246 | return type; 247 | } 248 | 249 | public boolean isArray() { 250 | return type.endsWith("[]"); 251 | } 252 | 253 | @Override 254 | public int compareTo(ArgumentAnnotatedField o) { 255 | return getVariableName().compareTo(o.getVariableName()); 256 | } 257 | 258 | public boolean isPrimitive() { 259 | return element.asType().getKind().isPrimitive(); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /processor/src/main/java/com/hannesdorfmann/fragmentargs/processor/ModifierUtils.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import java.util.Set; 4 | import javax.lang.model.element.Element; 5 | import javax.lang.model.element.Modifier; 6 | 7 | /** 8 | * Utility class to check modifiers 9 | * 10 | * @author Hannes Dorfmann 11 | */ 12 | public class ModifierUtils { 13 | 14 | private ModifierUtils() { 15 | 16 | } 17 | 18 | 19 | /** 20 | * Compare the modifier of two elements 21 | * 22 | * @return -1 if element a has better visibility, 0 if both have the same visibility, +1 if b has 23 | * the better visibility. The "best" visibility is PUBLIC 24 | */ 25 | public static int compareModifierVisibility(Element a, Element b) { 26 | 27 | 28 | // a better 29 | if (a.getModifiers().contains(Modifier.PUBLIC) && !b.getModifiers().contains(Modifier.PUBLIC)) { 30 | return -1; 31 | } 32 | 33 | if (isDefaultModifier(a.getModifiers()) && !isDefaultModifier(b.getModifiers())) { 34 | return -1; 35 | } 36 | 37 | if (a.getModifiers().contains(Modifier.PROTECTED) && !b.getModifiers().contains(Modifier.PROTECTED)) { 38 | return -1; 39 | } 40 | 41 | 42 | // b better 43 | if (b.getModifiers().contains(Modifier.PUBLIC) && !a.getModifiers().contains(Modifier.PUBLIC)) { 44 | return 1; 45 | } 46 | 47 | if (isDefaultModifier(b.getModifiers()) && !isDefaultModifier(a.getModifiers())) { 48 | return 1; 49 | } 50 | 51 | if (b.getModifiers().contains(Modifier.PROTECTED) && !a.getModifiers().contains(Modifier.PROTECTED)) { 52 | return 1; 53 | } 54 | 55 | // Same 56 | return 0; 57 | } 58 | 59 | /** 60 | * @param modifiers Set of modifiers 61 | * @return true if this element has default visibility 62 | */ 63 | public static boolean isDefaultModifier(Set modifiers) { 64 | return !modifiers.contains(Modifier.PUBLIC) && !modifiers.contains(Modifier.PROTECTED) && !modifiers.contains(Modifier.PRIVATE); 65 | } 66 | 67 | 68 | } 69 | -------------------------------------------------------------------------------- /processor/src/main/java/com/hannesdorfmann/fragmentargs/processor/ProcessingException.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import javax.lang.model.element.Element; 4 | 5 | /** 6 | * A simple exception that will be thrown if something went wrong (error message will be printed 7 | * before throwing exception) 8 | * 9 | * @author Hannes Dorfmann 10 | */ 11 | public class ProcessingException extends Exception { 12 | 13 | private Element element; 14 | private String message; 15 | private Object[] messageArgs; 16 | 17 | public ProcessingException(Element element, String message, Object... messageArgs) { 18 | this.element = element; 19 | this.message = message; 20 | this.messageArgs = messageArgs; 21 | } 22 | 23 | public Element getElement() { 24 | return element; 25 | } 26 | 27 | @Override public String getMessage() { 28 | return message; 29 | } 30 | 31 | public Object[] getMessageArgs() { 32 | return messageArgs; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /processor/src/main/java/com/hannesdorfmann/fragmentargs/repacked/com/squareup/javawriter/JavaWriter.java: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Square, Inc. 2 | package com.hannesdorfmann.fragmentargs.repacked.com.squareup.javawriter; 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | import java.io.Writer; 7 | import java.lang.annotation.Annotation; 8 | import java.util.ArrayDeque; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Collection; 12 | import java.util.Collections; 13 | import java.util.Deque; 14 | import java.util.EnumSet; 15 | import java.util.Iterator; 16 | import java.util.LinkedHashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Map.Entry; 20 | import java.util.Set; 21 | import java.util.TreeSet; 22 | import java.util.regex.Matcher; 23 | import java.util.regex.Pattern; 24 | import javax.lang.model.element.Modifier; 25 | 26 | import static javax.lang.model.element.Modifier.ABSTRACT; 27 | 28 | /** A utility class which aids in generating Java source files. */ 29 | public class JavaWriter implements Closeable { 30 | private static final Pattern TYPE_PATTERN = Pattern.compile("(?:[\\w$]+\\.)*([\\w\\.*$]+)"); 31 | private static final int MAX_SINGLE_LINE_ATTRIBUTES = 3; 32 | private static final String INDENT = " "; 33 | 34 | /** Map fully qualified type names to their short names. */ 35 | private final Map importedTypes = new LinkedHashMap(); 36 | 37 | private String packagePrefix; 38 | private final Deque scopes = new ArrayDeque(); 39 | private final Deque types = new ArrayDeque(); 40 | private final Writer out; 41 | private boolean isCompressingTypes = true; 42 | private String indent = INDENT; 43 | 44 | /** 45 | * @param out the stream to which Java source will be written. This should be a buffered stream. 46 | */ 47 | public JavaWriter(Writer out) { 48 | this.out = out; 49 | } 50 | 51 | public void setCompressingTypes(boolean isCompressingTypes) { 52 | this.isCompressingTypes = isCompressingTypes; 53 | } 54 | 55 | public boolean isCompressingTypes() { 56 | return isCompressingTypes; 57 | } 58 | 59 | public void setIndent(String indent) { 60 | this.indent = indent; 61 | } 62 | 63 | public String getIndent() { 64 | return indent; 65 | } 66 | 67 | /** Emit a package declaration and empty line. */ 68 | public JavaWriter emitPackage(String packageName) throws IOException { 69 | if (this.packagePrefix != null) { 70 | throw new IllegalStateException(); 71 | } 72 | if (packageName.isEmpty()) { 73 | this.packagePrefix = ""; 74 | } else { 75 | out.write("package "); 76 | out.write(packageName); 77 | out.write(";\n\n"); 78 | this.packagePrefix = packageName + "."; 79 | } 80 | return this; 81 | } 82 | 83 | /** 84 | * Emit an import for each {@code type} provided. For the duration of the file, all references to 85 | * these classes will be automatically shortened. 86 | */ 87 | public JavaWriter emitImports(String... types) throws IOException { 88 | return emitImports(Arrays.asList(types)); 89 | } 90 | 91 | /** 92 | * Emit an import for each {@code type} provided. For the duration of the file, all references to 93 | * these classes will be automatically shortened. 94 | */ 95 | public JavaWriter emitImports(Class... types) throws IOException { 96 | List classNames = new ArrayList(types.length); 97 | for (Class classToImport : types) { 98 | classNames.add(classToImport.getName()); 99 | } 100 | return emitImports(classNames); 101 | } 102 | 103 | /** 104 | * Emit an import for each {@code type} in the provided {@code Collection}. For the duration of 105 | * the file, all references to these classes will be automatically shortened. 106 | */ 107 | public JavaWriter emitImports(Collection types) throws IOException { 108 | for (String type : new TreeSet(types)) { 109 | Matcher matcher = TYPE_PATTERN.matcher(type); 110 | if (!matcher.matches()) { 111 | throw new IllegalArgumentException(type); 112 | } 113 | if (importedTypes.put(type, matcher.group(1)) != null) { 114 | throw new IllegalArgumentException(type); 115 | } 116 | out.write("import "); 117 | out.write(type); 118 | out.write(";\n"); 119 | } 120 | return this; 121 | } 122 | 123 | /** 124 | * Emit a static import for each {@code type} provided. For the duration of the file, 125 | * all references to these classes will be automatically shortened. 126 | */ 127 | public JavaWriter emitStaticImports(String... types) throws IOException { 128 | return emitStaticImports(Arrays.asList(types)); 129 | } 130 | 131 | /** 132 | * Emit a static import for each {@code type} in the provided {@code Collection}. For the 133 | * duration of the file, all references to these classes will be automatically shortened. 134 | */ 135 | public JavaWriter emitStaticImports(Collection types) throws IOException { 136 | for (String type : new TreeSet(types)) { 137 | Matcher matcher = TYPE_PATTERN.matcher(type); 138 | if (!matcher.matches()) { 139 | throw new IllegalArgumentException(type); 140 | } 141 | if (importedTypes.put(type, matcher.group(1)) != null) { 142 | throw new IllegalArgumentException(type); 143 | } 144 | out.write("import static "); 145 | out.write(type); 146 | out.write(";\n"); 147 | } 148 | return this; 149 | } 150 | 151 | /** 152 | * Emits a name like {@code java.lang.String} or {@code java.util.List}, 153 | * compressing it with imports if possible. Type compression will only be enabled if 154 | * {@link #isCompressingTypes} is true. 155 | */ 156 | private JavaWriter emitCompressedType(String type) throws IOException { 157 | if (isCompressingTypes) { 158 | out.write(compressType(type)); 159 | } else { 160 | out.write(type); 161 | } 162 | return this; 163 | } 164 | 165 | /** Try to compress a fully-qualified class name to only the class name. */ 166 | public String compressType(String type) { 167 | StringBuilder sb = new StringBuilder(); 168 | if (this.packagePrefix == null) { 169 | throw new IllegalStateException(); 170 | } 171 | 172 | Matcher m = TYPE_PATTERN.matcher(type); 173 | int pos = 0; 174 | while (true) { 175 | boolean found = m.find(pos); 176 | 177 | // Copy non-matching characters like "<". 178 | int typeStart = found ? m.start() : type.length(); 179 | sb.append(type, pos, typeStart); 180 | 181 | if (!found) { 182 | break; 183 | } 184 | 185 | // Copy a single class name, shortening it if possible. 186 | String name = m.group(0); 187 | String imported = importedTypes.get(name); 188 | if (imported != null) { 189 | sb.append(imported); 190 | } else if (isClassInPackage(name, packagePrefix)) { 191 | String compressed = name.substring(packagePrefix.length()); 192 | if (isAmbiguous(compressed)) { 193 | sb.append(name); 194 | } else { 195 | sb.append(compressed); 196 | } 197 | } else if (isClassInPackage(name, "java.lang.")) { 198 | sb.append(name.substring("java.lang.".length())); 199 | } else { 200 | sb.append(name); 201 | } 202 | pos = m.end(); 203 | } 204 | return sb.toString(); 205 | } 206 | 207 | private static boolean isClassInPackage(String name, String packagePrefix) { 208 | if (name.startsWith(packagePrefix)) { 209 | if (name.indexOf('.', packagePrefix.length()) == -1) { 210 | return true; 211 | } 212 | // check to see if the part after the package looks like a class 213 | if (Character.isUpperCase(name.charAt(packagePrefix.length()))) { 214 | return true; 215 | } 216 | } 217 | return false; 218 | } 219 | 220 | /** 221 | * Returns true if the imports contain a class with same simple name as {@code compressed}. 222 | * 223 | * @param compressed simple name of the type 224 | */ 225 | private boolean isAmbiguous(String compressed) { 226 | return importedTypes.values().contains(compressed); 227 | } 228 | 229 | /** 230 | * Emits an initializer declaration. 231 | * 232 | * @param isStatic true if it should be an static initializer, false for an instance initializer. 233 | */ 234 | public JavaWriter beginInitializer(boolean isStatic) throws IOException { 235 | indent(); 236 | if (isStatic) { 237 | out.write("static"); 238 | out.write(" {\n"); 239 | } else { 240 | out.write("{\n"); 241 | } 242 | scopes.push(Scope.INITIALIZER); 243 | return this; 244 | } 245 | 246 | /** Ends the current initializer declaration. */ 247 | public JavaWriter endInitializer() throws IOException { 248 | popScope(Scope.INITIALIZER); 249 | indent(); 250 | out.write("}\n"); 251 | return this; 252 | } 253 | 254 | /** 255 | * Emits a type declaration. 256 | * 257 | * @param kind such as "class", "interface" or "enum". 258 | */ 259 | public JavaWriter beginType(String type, String kind) throws IOException { 260 | return beginType(type, kind, EnumSet.noneOf(Modifier.class), null); 261 | } 262 | 263 | /** 264 | * Emits a type declaration. 265 | * 266 | * @param kind such as "class", "interface" or "enum". 267 | */ 268 | public JavaWriter beginType(String type, String kind, Set modifiers) 269 | throws IOException { 270 | return beginType(type, kind, modifiers, null); 271 | } 272 | 273 | /** 274 | * Emits a type declaration. 275 | * 276 | * @param kind such as "class", "interface" or "enum". 277 | * @param extendsType the class to extend, or null for no extends clause. 278 | */ 279 | public JavaWriter beginType(String type, String kind, Set modifiers, String extendsType, 280 | String... implementsTypes) throws IOException { 281 | indent(); 282 | emitModifiers(modifiers); 283 | out.write(kind); 284 | out.write(" "); 285 | emitCompressedType(type); 286 | if (extendsType != null) { 287 | out.write(" extends "); 288 | emitCompressedType(extendsType); 289 | } 290 | if (implementsTypes.length > 0) { 291 | out.write("\n"); 292 | indent(); 293 | out.write(" implements "); 294 | for (int i = 0; i < implementsTypes.length; i++) { 295 | if (i != 0) { 296 | out.write(", "); 297 | } 298 | emitCompressedType(implementsTypes[i]); 299 | } 300 | } 301 | out.write(" {\n"); 302 | scopes.push("interface".equals(kind) ? Scope.INTERFACE_DECLARATION : Scope.TYPE_DECLARATION); 303 | types.push(type); 304 | return this; 305 | } 306 | 307 | /** Completes the current type declaration. */ 308 | public JavaWriter endType() throws IOException { 309 | popScope(Scope.TYPE_DECLARATION, Scope.INTERFACE_DECLARATION); 310 | types.pop(); 311 | indent(); 312 | out.write("}\n"); 313 | return this; 314 | } 315 | 316 | /** Emits a field declaration. */ 317 | public JavaWriter emitField(String type, String name) throws IOException { 318 | return emitField(type, name, EnumSet.noneOf(Modifier.class), null); 319 | } 320 | 321 | /** Emits a field declaration. */ 322 | public JavaWriter emitField(String type, String name, Set modifiers) 323 | throws IOException { 324 | return emitField(type, name, modifiers, null); 325 | } 326 | 327 | /** Emits a field declaration. */ 328 | public JavaWriter emitField(String type, String name, Set modifiers, 329 | String initialValue) throws IOException { 330 | indent(); 331 | emitModifiers(modifiers); 332 | emitCompressedType(type); 333 | out.write(" "); 334 | out.write(name); 335 | 336 | if (initialValue != null) { 337 | out.write(" ="); 338 | if (!initialValue.startsWith("\n")) { 339 | out.write(" "); 340 | } 341 | 342 | String[] lines = initialValue.split("\n", -1); 343 | out.write(lines[0]); 344 | for (int i = 1; i < lines.length; i++) { 345 | out.write("\n"); 346 | hangingIndent(); 347 | out.write(lines[i]); 348 | } 349 | } 350 | out.write(";\n"); 351 | return this; 352 | } 353 | 354 | /** 355 | * Emit a method declaration. 356 | * 357 | *

A {@code null} return type may be used to indicate a constructor, but 358 | * {@link #beginConstructor(Set, String...)} should be preferred. This behavior may be removed in 359 | * a future release. 360 | * 361 | * @param returnType the method's return type, or null for constructors 362 | * @param name the method name, or the fully qualified class name for constructors. 363 | * @param modifiers the set of modifiers to be applied to the method 364 | * @param parameters alternating parameter types and names. 365 | */ 366 | public JavaWriter beginMethod(String returnType, String name, Set modifiers, 367 | String... parameters) throws IOException { 368 | return beginMethod(returnType, name, modifiers, Arrays.asList(parameters), null); 369 | } 370 | 371 | /** 372 | * Emit a method declaration. 373 | * 374 | *

A {@code null} return type may be used to indicate a constructor, but 375 | * {@link #beginConstructor(Set, List, List)} should be preferred. This behavior may be removed in 376 | * a future release. 377 | * 378 | * @param returnType the method's return type, or null for constructors. 379 | * @param name the method name, or the fully qualified class name for constructors. 380 | * @param modifiers the set of modifiers to be applied to the method 381 | * @param parameters alternating parameter types and names. 382 | * @param throwsTypes the classes to throw, or null for no throws clause. 383 | */ 384 | public JavaWriter beginMethod(String returnType, String name, Set modifiers, 385 | List parameters, List throwsTypes) throws IOException { 386 | indent(); 387 | emitModifiers(modifiers); 388 | if (returnType != null) { 389 | emitCompressedType(returnType); 390 | out.write(" "); 391 | out.write(name); 392 | } else { 393 | emitCompressedType(name); 394 | } 395 | out.write("("); 396 | if (parameters != null) { 397 | for (int p = 0; p < parameters.size();) { 398 | if (p != 0) { 399 | out.write(", "); 400 | } 401 | emitCompressedType(parameters.get(p++)); 402 | out.write(" "); 403 | emitCompressedType(parameters.get(p++)); 404 | } 405 | } 406 | out.write(")"); 407 | if (throwsTypes != null && throwsTypes.size() > 0) { 408 | out.write("\n"); 409 | indent(); 410 | out.write(" throws "); 411 | for (int i = 0; i < throwsTypes.size(); i++) { 412 | if (i != 0) { 413 | out.write(", "); 414 | } 415 | emitCompressedType(throwsTypes.get(i)); 416 | } 417 | } 418 | if (modifiers.contains(ABSTRACT) || Scope.INTERFACE_DECLARATION.equals(scopes.peek())) { 419 | out.write(";\n"); 420 | scopes.push(Scope.ABSTRACT_METHOD); 421 | } else { 422 | out.write(" {\n"); 423 | scopes.push(returnType == null ? Scope.CONSTRUCTOR : Scope.NON_ABSTRACT_METHOD); 424 | } 425 | return this; 426 | } 427 | 428 | public JavaWriter beginConstructor(Set modifiers, String... parameters) 429 | throws IOException { 430 | beginMethod(null, rawType(types.peekFirst()), modifiers, parameters); 431 | return this; 432 | } 433 | 434 | public JavaWriter beginConstructor(Set modifiers, 435 | List parameters, List throwsTypes) 436 | throws IOException { 437 | beginMethod(null, rawType(types.peekFirst()), modifiers, parameters, throwsTypes); 438 | return this; 439 | } 440 | 441 | /** Emits some Javadoc comments with line separated by {@code \n}. */ 442 | public JavaWriter emitJavadoc(String javadoc, Object... params) throws IOException { 443 | String formatted = String.format(javadoc, params); 444 | 445 | indent(); 446 | out.write("/**\n"); 447 | for (String line : formatted.split("\n")) { 448 | indent(); 449 | out.write(" *"); 450 | if (!line.isEmpty()) { 451 | out.write(" "); 452 | out.write(line); 453 | } 454 | out.write("\n"); 455 | } 456 | indent(); 457 | out.write(" */\n"); 458 | return this; 459 | } 460 | 461 | /** Emits a single line comment. */ 462 | public JavaWriter emitSingleLineComment(String comment, Object... args) throws IOException { 463 | indent(); 464 | out.write("// "); 465 | out.write(String.format(comment, args)); 466 | out.write("\n"); 467 | return this; 468 | } 469 | 470 | public JavaWriter emitEmptyLine() throws IOException { 471 | out.write("\n"); 472 | return this; 473 | } 474 | 475 | public JavaWriter emitEnumValue(String name) throws IOException { 476 | indent(); 477 | out.write(name); 478 | out.write(",\n"); 479 | return this; 480 | } 481 | 482 | /** 483 | * A simple switch to emit the proper enum depending if its last causing it to be terminated 484 | * by a semi-colon ({@code ;}). 485 | */ 486 | public JavaWriter emitEnumValue(String name, boolean isLast) throws IOException { 487 | return isLast ? emitLastEnumValue(name) : emitEnumValue(name); 488 | } 489 | 490 | private JavaWriter emitLastEnumValue(String name) throws IOException { 491 | indent(); 492 | out.write(name); 493 | out.write(";\n"); 494 | return this; 495 | } 496 | 497 | /** Emit a list of enum values followed by a semi-colon ({@code ;}). */ 498 | public JavaWriter emitEnumValues(Iterable names) throws IOException { 499 | Iterator iterator = names.iterator(); 500 | 501 | while (iterator.hasNext()) { 502 | String name = iterator.next(); 503 | if (iterator.hasNext()) { 504 | emitEnumValue(name); 505 | } else { 506 | emitLastEnumValue(name); 507 | } 508 | } 509 | 510 | return this; 511 | } 512 | 513 | /** Equivalent to {@code annotation(annotation, emptyMap())}. */ 514 | public JavaWriter emitAnnotation(String annotation) throws IOException { 515 | return emitAnnotation(annotation, Collections.emptyMap()); 516 | } 517 | 518 | /** Equivalent to {@code annotation(annotationType.getName(), emptyMap())}. */ 519 | public JavaWriter emitAnnotation(Class annotationType) throws IOException { 520 | return emitAnnotation(type(annotationType), Collections.emptyMap()); 521 | } 522 | 523 | /** 524 | * Annotates the next element with {@code annotationType} and a {@code value}. 525 | * 526 | * @param value an object used as the default (value) parameter of the annotation. The value will 527 | * be encoded using Object.toString(); use {@link #stringLiteral} for String values. Object 528 | * arrays are written one element per line. 529 | */ 530 | public JavaWriter emitAnnotation(Class annotationType, Object value) 531 | throws IOException { 532 | return emitAnnotation(type(annotationType), value); 533 | } 534 | 535 | /** 536 | * Annotates the next element with {@code annotation} and a {@code value}. 537 | * 538 | * @param value an object used as the default (value) parameter of the annotation. The value will 539 | * be encoded using Object.toString(); use {@link #stringLiteral} for String values. Object 540 | * arrays are written one element per line. 541 | */ 542 | public JavaWriter emitAnnotation(String annotation, Object value) throws IOException { 543 | indent(); 544 | out.write("@"); 545 | emitCompressedType(annotation); 546 | out.write("("); 547 | emitAnnotationValue(value); 548 | out.write(")"); 549 | out.write("\n"); 550 | return this; 551 | } 552 | 553 | /** Equivalent to {@code annotation(annotationType.getName(), attributes)}. */ 554 | public JavaWriter emitAnnotation(Class annotationType, 555 | Map attributes) throws IOException { 556 | return emitAnnotation(type(annotationType), attributes); 557 | } 558 | 559 | /** 560 | * Annotates the next element with {@code annotation} and {@code attributes}. 561 | * 562 | * @param attributes a map from annotation attribute names to their values. Values are encoded 563 | * using Object.toString(); use {@link #stringLiteral} for String values. Object arrays are 564 | * written one element per line. 565 | */ 566 | public JavaWriter emitAnnotation(String annotation, Map attributes) 567 | throws IOException { 568 | indent(); 569 | out.write("@"); 570 | emitCompressedType(annotation); 571 | switch (attributes.size()) { 572 | case 0: 573 | break; 574 | case 1: 575 | Entry onlyEntry = attributes.entrySet().iterator().next(); 576 | out.write("("); 577 | if (!"value".equals(onlyEntry.getKey())) { 578 | out.write(onlyEntry.getKey()); 579 | out.write(" = "); 580 | } 581 | emitAnnotationValue(onlyEntry.getValue()); 582 | out.write(")"); 583 | break; 584 | default: 585 | boolean split = attributes.size() > MAX_SINGLE_LINE_ATTRIBUTES 586 | || containsArray(attributes.values()); 587 | out.write("("); 588 | scopes.push(Scope.ANNOTATION_ATTRIBUTE); 589 | String separator = split ? "\n" : ""; 590 | for (Map.Entry entry : attributes.entrySet()) { 591 | out.write(separator); 592 | separator = split ? ",\n" : ", "; 593 | if (split) { 594 | indent(); 595 | } 596 | out.write(entry.getKey()); 597 | out.write(" = "); 598 | Object value = entry.getValue(); 599 | emitAnnotationValue(value); 600 | } 601 | popScope(Scope.ANNOTATION_ATTRIBUTE); 602 | if (split) { 603 | out.write("\n"); 604 | indent(); 605 | } 606 | out.write(")"); 607 | break; 608 | } 609 | out.write("\n"); 610 | return this; 611 | } 612 | 613 | private boolean containsArray(Collection values) { 614 | for (Object value : values) { 615 | if (value instanceof Object[]) { 616 | return true; 617 | } 618 | } 619 | return false; 620 | } 621 | 622 | /** 623 | * Writes a single annotation value. If the value is an array, each element in the array will be 624 | * written to its own line. 625 | */ 626 | private JavaWriter emitAnnotationValue(Object value) throws IOException { 627 | if (value instanceof Object[]) { 628 | out.write("{"); 629 | boolean firstValue = true; 630 | scopes.push(Scope.ANNOTATION_ARRAY_VALUE); 631 | for (Object o : ((Object[]) value)) { 632 | if (firstValue) { 633 | firstValue = false; 634 | out.write("\n"); 635 | } else { 636 | out.write(",\n"); 637 | } 638 | indent(); 639 | out.write(o.toString()); 640 | } 641 | popScope(Scope.ANNOTATION_ARRAY_VALUE); 642 | out.write("\n"); 643 | indent(); 644 | out.write("}"); 645 | } else { 646 | out.write(value.toString()); 647 | } 648 | return this; 649 | } 650 | 651 | /** 652 | * @param pattern a code pattern like "int i = %s". Newlines will be further indented. Should not 653 | * contain trailing semicolon. 654 | */ 655 | public JavaWriter emitStatement(String pattern, Object... args) throws IOException { 656 | checkInMethod(); 657 | String[] lines = String.format(pattern, args).split("\n", -1); 658 | indent(); 659 | out.write(lines[0]); 660 | for (int i = 1; i < lines.length; i++) { 661 | out.write("\n"); 662 | hangingIndent(); 663 | out.write(lines[i]); 664 | } 665 | out.write(";\n"); 666 | return this; 667 | } 668 | 669 | /** 670 | * @param controlFlow the control flow construct and its code, such as "if (foo == 5)". Shouldn't 671 | * contain braces or newline characters. 672 | */ 673 | public JavaWriter beginControlFlow(String controlFlow, Object... args) throws IOException { 674 | checkInMethod(); 675 | indent(); 676 | out.write(String.format(controlFlow, args)); 677 | out.write(" {\n"); 678 | scopes.push(Scope.CONTROL_FLOW); 679 | return this; 680 | } 681 | 682 | /** 683 | * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". 684 | * Shouldn't contain braces or newline characters. 685 | */ 686 | public JavaWriter nextControlFlow(String controlFlow, Object... args) throws IOException { 687 | popScope(Scope.CONTROL_FLOW); 688 | indent(); 689 | scopes.push(Scope.CONTROL_FLOW); 690 | out.write("} "); 691 | out.write(String.format(controlFlow, args)); 692 | out.write(" {\n"); 693 | return this; 694 | } 695 | 696 | public JavaWriter endControlFlow() throws IOException { 697 | return endControlFlow(null); 698 | } 699 | 700 | /** 701 | * @param controlFlow the optional control flow construct and its code, such as 702 | * "while(foo == 20)". Only used for "do/while" control flows. 703 | */ 704 | public JavaWriter endControlFlow(String controlFlow, Object... args) throws IOException { 705 | popScope(Scope.CONTROL_FLOW); 706 | indent(); 707 | if (controlFlow != null) { 708 | out.write("} "); 709 | out.write(String.format(controlFlow, args)); 710 | out.write(";\n"); 711 | } else { 712 | out.write("}\n"); 713 | } 714 | return this; 715 | } 716 | 717 | /** Completes the current method declaration. */ 718 | public JavaWriter endMethod() throws IOException { 719 | Scope popped = scopes.pop(); 720 | // support calling a constructor a "method" to support the legacy code 721 | if (popped == Scope.NON_ABSTRACT_METHOD || popped == Scope.CONSTRUCTOR) { 722 | indent(); 723 | out.write("}\n"); 724 | } else if (popped != Scope.ABSTRACT_METHOD) { 725 | throw new IllegalStateException(); 726 | } 727 | return this; 728 | } 729 | 730 | /** Completes the current constructor declaration. */ 731 | public JavaWriter endConstructor() throws IOException { 732 | popScope(Scope.CONSTRUCTOR); 733 | indent(); 734 | out.write("}\n"); 735 | return this; 736 | } 737 | 738 | /** Returns the string literal representing {@code data}, including wrapping quotes. */ 739 | public static String stringLiteral(String data) { 740 | StringBuilder result = new StringBuilder(); 741 | result.append('"'); 742 | for (int i = 0; i < data.length(); i++) { 743 | char c = data.charAt(i); 744 | switch (c) { 745 | case '"': 746 | result.append("\\\""); 747 | break; 748 | case '\\': 749 | result.append("\\\\"); 750 | break; 751 | case '\b': 752 | result.append("\\b"); 753 | break; 754 | case '\t': 755 | result.append("\\t"); 756 | break; 757 | case '\n': 758 | result.append("\\n"); 759 | break; 760 | case '\f': 761 | result.append("\\f"); 762 | break; 763 | case '\r': 764 | result.append("\\r"); 765 | break; 766 | default: 767 | if (Character.isISOControl(c)) { 768 | result.append(String.format("\\u%04x", (int) c)); 769 | } else { 770 | result.append(c); 771 | } 772 | } 773 | } 774 | result.append('"'); 775 | return result.toString(); 776 | } 777 | 778 | /** Build a string representation of a type and optionally its generic type arguments. */ 779 | public static String type(Class raw, String... parameters) { 780 | if (parameters.length == 0) { 781 | return raw.getCanonicalName(); 782 | } 783 | if (raw.getTypeParameters().length != parameters.length) { 784 | throw new IllegalArgumentException(); 785 | } 786 | StringBuilder result = new StringBuilder(); 787 | result.append(raw.getCanonicalName()); 788 | result.append("<"); 789 | result.append(parameters[0]); 790 | for (int i = 1; i < parameters.length; i++) { 791 | result.append(", "); 792 | result.append(parameters[i]); 793 | } 794 | result.append(">"); 795 | return result.toString(); 796 | } 797 | 798 | /** Build a string representation of the raw type for a (optionally generic) type. */ 799 | public static String rawType(String type) { 800 | int lessThanIndex = type.indexOf('<'); 801 | if (lessThanIndex != -1) { 802 | return type.substring(0, lessThanIndex); 803 | } 804 | return type; 805 | } 806 | 807 | @Override public void close() throws IOException { 808 | out.close(); 809 | } 810 | 811 | /** Emits the modifiers to the writer. */ 812 | private void emitModifiers(Set modifiers) throws IOException { 813 | if (modifiers.isEmpty()) { 814 | return; 815 | } 816 | // Use an EnumSet to ensure the proper ordering 817 | if (!(modifiers instanceof EnumSet)) { 818 | modifiers = EnumSet.copyOf(modifiers); 819 | } 820 | for (Modifier modifier : modifiers) { 821 | out.append(modifier.toString()).append(' '); 822 | } 823 | } 824 | 825 | private void indent() throws IOException { 826 | for (int i = 0, count = scopes.size(); i < count; i++) { 827 | out.write(indent); 828 | } 829 | } 830 | 831 | private void hangingIndent() throws IOException { 832 | for (int i = 0, count = scopes.size() + 2; i < count; i++) { 833 | out.write(indent); 834 | } 835 | } 836 | 837 | private static final EnumSet 838 | METHOD_SCOPES = EnumSet.of(Scope.NON_ABSTRACT_METHOD, Scope.CONSTRUCTOR, Scope.CONTROL_FLOW, 839 | Scope.INITIALIZER); 840 | 841 | private void checkInMethod() { 842 | if (!METHOD_SCOPES.contains(scopes.peekFirst())) { 843 | throw new IllegalArgumentException(); 844 | } 845 | } 846 | 847 | private void popScope(Scope... expected) { 848 | if (!EnumSet.copyOf(Arrays.asList(expected)).contains(scopes.pop())) { 849 | throw new IllegalStateException(); 850 | } 851 | } 852 | 853 | private enum Scope { 854 | TYPE_DECLARATION, 855 | INTERFACE_DECLARATION, 856 | ABSTRACT_METHOD, 857 | NON_ABSTRACT_METHOD, 858 | CONSTRUCTOR, 859 | CONTROL_FLOW, 860 | ANNOTATION_ATTRIBUTE, 861 | ANNOTATION_ARRAY_VALUE, 862 | INITIALIZER 863 | } 864 | } 865 | -------------------------------------------------------------------------------- /processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | com.hannesdorfmann.fragmentargs.processor.ArgProcessor -------------------------------------------------------------------------------- /processor/src/test/java/com/hannesdorfmann/fragmentargs/processor/CompareModifierUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import javax.lang.model.element.Element; 6 | import javax.lang.model.element.Modifier; 7 | import junit.framework.Assert; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.mockito.Mockito; 11 | 12 | /** 13 | * @author Hannes Dorfmann 14 | */ 15 | public class CompareModifierUtilsTest { 16 | 17 | Element a, b; 18 | Set aModifiers, bModifiers; 19 | 20 | @Before 21 | public void init() { 22 | a = Mockito.mock(Element.class); 23 | b = Mockito.mock(Element.class); 24 | 25 | aModifiers = new HashSet(); 26 | bModifiers = new HashSet(); 27 | 28 | Mockito.doReturn(aModifiers).when(a).getModifiers(); 29 | Mockito.doReturn(bModifiers).when(b).getModifiers(); 30 | } 31 | 32 | @Test 33 | public void aPublicBnot() { 34 | aModifiers.add(Modifier.PUBLIC); 35 | bModifiers.add(Modifier.PRIVATE); 36 | 37 | Assert.assertEquals(-1, ModifierUtils.compareModifierVisibility(a, b)); 38 | } 39 | 40 | @Test 41 | public void aDefaultBnot() { 42 | bModifiers.add(Modifier.PRIVATE); 43 | 44 | Assert.assertEquals(-1, ModifierUtils.compareModifierVisibility(a, b)); 45 | } 46 | 47 | @Test 48 | public void aProtectedBnot() { 49 | aModifiers.add(Modifier.PROTECTED); 50 | bModifiers.add(Modifier.PRIVATE); 51 | 52 | Assert.assertEquals(-1, ModifierUtils.compareModifierVisibility(a, b)); 53 | } 54 | 55 | 56 | @Test 57 | public void bPublicAnot() { 58 | bModifiers.add(Modifier.PUBLIC); 59 | aModifiers.add(Modifier.PRIVATE); 60 | 61 | Assert.assertEquals(1, ModifierUtils.compareModifierVisibility(a, b)); 62 | } 63 | 64 | @Test 65 | public void bDefaultAnot() { 66 | aModifiers.add(Modifier.PRIVATE); 67 | 68 | Assert.assertEquals(1, ModifierUtils.compareModifierVisibility(a, b)); 69 | } 70 | 71 | @Test 72 | public void bProtectedAnot() { 73 | aModifiers.add(Modifier.PRIVATE); 74 | bModifiers.add(Modifier.PROTECTED); 75 | 76 | Assert.assertEquals(1, ModifierUtils.compareModifierVisibility(a, b)); 77 | } 78 | 79 | @Test 80 | public void samePrivate() { 81 | aModifiers.add(Modifier.PRIVATE); 82 | bModifiers.add(Modifier.PRIVATE); 83 | 84 | Assert.assertEquals(0, ModifierUtils.compareModifierVisibility(a, b)); 85 | } 86 | 87 | @Test 88 | public void sameProtected() { 89 | aModifiers.add(Modifier.PRIVATE); 90 | bModifiers.add(Modifier.PRIVATE); 91 | 92 | Assert.assertEquals(0, ModifierUtils.compareModifierVisibility(a, b)); 93 | } 94 | 95 | @Test 96 | public void sameDefault() { 97 | 98 | Assert.assertEquals(0, ModifierUtils.compareModifierVisibility(a, b)); 99 | } 100 | 101 | @Test 102 | public void samePublic() { 103 | aModifiers.add(Modifier.PUBLIC); 104 | bModifiers.add(Modifier.PUBLIC); 105 | 106 | Assert.assertEquals(0, ModifierUtils.compareModifierVisibility(a, b)); 107 | } 108 | 109 | 110 | 111 | 112 | } 113 | -------------------------------------------------------------------------------- /processor/src/test/java/com/hannesdorfmann/fragmentargs/processor/CompileTest.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import com.google.testing.compile.JavaFileObjects; 4 | 5 | import static com.google.common.truth.Truth.assert_; 6 | import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; 7 | 8 | public final class CompileTest { 9 | 10 | public static void assertClassCompilesWithoutError(final String classResourceName, final String outputClassResourceName) { 11 | assert_().about(javaSource()) 12 | .that(JavaFileObjects.forResource(classResourceName)) 13 | .processedWith(new ArgProcessor()) 14 | .compilesWithoutError() 15 | .and() 16 | .generatesSources(JavaFileObjects.forResource(outputClassResourceName)); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /processor/src/test/java/com/hannesdorfmann/fragmentargs/processor/InnerClassTest.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import org.junit.Test; 4 | 5 | import static com.hannesdorfmann.fragmentargs.processor.CompileTest.assertClassCompilesWithoutError; 6 | 7 | public class InnerClassTest { 8 | 9 | @Test 10 | public void innerClass() { 11 | assertClassCompilesWithoutError("ClassWithInnerClass.java", "ClassWithInnerClassBuilder.java"); 12 | } 13 | 14 | @Test 15 | public void innerClassWithProtectedField() { 16 | assertClassCompilesWithoutError("InnerClassWithProtectedField.java", "InnerClassWithProtectedFieldBuilder.java"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /processor/src/test/java/com/hannesdorfmann/fragmentargs/processor/ModifierUtilsDefaultModifierTest.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import javax.lang.model.element.Modifier; 6 | import junit.framework.Assert; 7 | import org.junit.Test; 8 | 9 | /** 10 | * @author Hannes Dorfmann 11 | */ 12 | public class ModifierUtilsDefaultModifierTest { 13 | 14 | @Test 15 | public void isDefaultModifier() { 16 | 17 | Set modifiers = new HashSet(); 18 | modifiers.add(Modifier.PUBLIC); 19 | Assert.assertFalse(ModifierUtils.isDefaultModifier(modifiers)); 20 | 21 | modifiers.clear(); 22 | modifiers.add(Modifier.PRIVATE); 23 | Assert.assertFalse(ModifierUtils.isDefaultModifier(modifiers)); 24 | 25 | modifiers.clear(); 26 | modifiers.add(Modifier.PROTECTED); 27 | Assert.assertFalse(ModifierUtils.isDefaultModifier(modifiers)); 28 | 29 | 30 | modifiers.clear(); 31 | modifiers.add(Modifier.FINAL); 32 | modifiers.add(Modifier.STATIC); 33 | 34 | Assert.assertTrue(ModifierUtils.isDefaultModifier(modifiers)); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /processor/src/test/java/com/hannesdorfmann/fragmentargs/processor/ProtectedAccessTest.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor; 2 | 3 | import com.google.testing.compile.JavaFileObjects; 4 | 5 | import org.junit.Test; 6 | 7 | import static com.google.common.truth.Truth.assert_; 8 | import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; 9 | 10 | public class ProtectedAccessTest { 11 | 12 | private static final String[] PROCESSOR_OPTIONS 13 | = new String[]{"-AfragmentArgsSupportAnnotations=false"}; 14 | 15 | @Test 16 | public void protectedField() { 17 | assert_().about(javaSource()) 18 | .that(JavaFileObjects.forResource("ClassWithProtectedField.java")) 19 | .withCompilerOptions(PROCESSOR_OPTIONS) 20 | .processedWith(new ArgProcessor()) 21 | .compilesWithoutError(); 22 | } 23 | 24 | @Test 25 | public void protectedSetter() { 26 | assert_().about(javaSource()) 27 | .that(JavaFileObjects.forResource("ClassWithProtectedSetter.java")) 28 | .withCompilerOptions(PROCESSOR_OPTIONS) 29 | .processedWith(new ArgProcessor()) 30 | .compilesWithoutError(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /processor/src/test/resources/ClassWithInnerClass.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor.test; 2 | 3 | @com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs 4 | public class ClassWithInnerClass extends android.app.Fragment { 5 | 6 | @com.hannesdorfmann.fragmentargs.annotation.Arg 7 | String arg; 8 | 9 | @com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs 10 | public static class InnerClass extends android.app.Fragment { 11 | 12 | @com.hannesdorfmann.fragmentargs.annotation.Arg 13 | String arg; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /processor/src/test/resources/ClassWithInnerClassBuilder.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor.test; 2 | 3 | import android.os.Bundle; 4 | 5 | public final class ClassWithInnerClassBuilder { 6 | 7 | private final Bundle mArguments = new Bundle(); 8 | 9 | public ClassWithInnerClassBuilder(String arg) { 10 | 11 | mArguments.putString("arg", arg); 12 | } 13 | 14 | public static ClassWithInnerClass newClassWithInnerClass(String arg) { 15 | return new ClassWithInnerClassBuilder(arg).build(); 16 | } 17 | 18 | public Bundle buildBundle() { 19 | return new Bundle(mArguments); 20 | } 21 | 22 | public static final void injectArguments(ClassWithInnerClass fragment) { 23 | Bundle args = fragment.getArguments(); 24 | if (args == null) { 25 | throw new IllegalStateException("No arguments set. Have you set up this Fragment with the corresponding FragmentArgs Builder? "); 26 | } 27 | 28 | if (!args.containsKey("arg")) { 29 | throw new IllegalStateException("required argument arg is not set"); 30 | } 31 | fragment.arg = args.getString("arg"); 32 | } 33 | 34 | public ClassWithInnerClass build() { 35 | ClassWithInnerClass fragment = new ClassWithInnerClass(); 36 | fragment.setArguments(mArguments); 37 | return fragment; 38 | } 39 | } -------------------------------------------------------------------------------- /processor/src/test/resources/ClassWithProtectedField.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor.test; 2 | 3 | @com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs 4 | public class ClassWithProtectedField extends android.app.Fragment { 5 | @com.hannesdorfmann.fragmentargs.annotation.Arg 6 | protected String protectedArg; 7 | } -------------------------------------------------------------------------------- /processor/src/test/resources/ClassWithProtectedSetter.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor.test; 2 | 3 | @com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs 4 | public class ClassWithProtectedSetter extends android.app.Fragment { 5 | @com.hannesdorfmann.fragmentargs.annotation.Arg 6 | private String privateArg; 7 | 8 | protected void setPrivateArg(String privateArg) { 9 | this.privateArg = privateArg; 10 | } 11 | } -------------------------------------------------------------------------------- /processor/src/test/resources/InnerClassWithProtectedField.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor.test; 2 | 3 | @com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs 4 | public class InnerClassWithProtectedField extends android.app.Fragment { 5 | 6 | @com.hannesdorfmann.fragmentargs.annotation.Arg 7 | protected String arg; 8 | 9 | @com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs 10 | public static class InnerClass extends android.app.Fragment { 11 | 12 | @com.hannesdorfmann.fragmentargs.annotation.Arg 13 | protected String arg; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /processor/src/test/resources/InnerClassWithProtectedFieldBuilder.java: -------------------------------------------------------------------------------- 1 | package com.hannesdorfmann.fragmentargs.processor.test; 2 | 3 | import android.os.Bundle; 4 | 5 | public final class InnerClassWithProtectedFieldBuilder { 6 | 7 | private final Bundle mArguments = new Bundle(); 8 | 9 | public InnerClassWithProtectedFieldBuilder(String arg) { 10 | 11 | mArguments.putString("arg", arg); 12 | } 13 | 14 | public static InnerClassWithProtectedField newInnerClassWithProtectedField(String arg) { 15 | return new InnerClassWithProtectedFieldBuilder(arg).build(); 16 | } 17 | 18 | public Bundle buildBundle() { 19 | return new Bundle(mArguments); 20 | } 21 | 22 | public static final void injectArguments(InnerClassWithProtectedField fragment) { 23 | Bundle args = fragment.getArguments(); 24 | if (args == null) { 25 | throw new IllegalStateException("No arguments set. Have you set up this Fragment with the corresponding FragmentArgs Builder? "); 26 | } 27 | 28 | if (!args.containsKey("arg")) { 29 | throw new IllegalStateException("required argument arg is not set"); 30 | } 31 | fragment.arg = args.getString("arg"); 32 | } 33 | 34 | public InnerClassWithProtectedField build() { 35 | InnerClassWithProtectedField fragment = new InnerClassWithProtectedField(); 36 | fragment.setArguments(mArguments); 37 | return fragment; 38 | } 39 | } --------------------------------------------------------------------------------