Subversion Repository Public Repository

litesoft

1
2
3
4
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
The primary purpose of this app was to demonstrate how to use the GWT concepts of Place/Activity/View with the RunAsync mechanism of GWT.
Thus allowing for splitting up the GWT generated JavaScript into a Core chunk and a number of load on demand (or Lazy Loaded) chunks.

The GWT Documentation Pages of interest are:

    GWT Development with Activities and Places
        http://code.google.com/webtoolkit/doc/latest/DevGuideMvpActivitiesAndPlaces.html

  and the two "Large scale application development and MVP" links in the top section of the above:

    Part 1: http://code.google.com/webtoolkit/articles/mvp-architecture.html
    Part 2: http://code.google.com/webtoolkit/articles/mvp-architecture-2.html

It is my understanding that direct calls between Lazy Loaded chunks (and from the Core to the Lazy Loaded chunks) was not possible, but
that once a Lazy Loaded chunk had registered an implementation of a Core interface with the Core, then either the Core or any Lazy Loaded chunk's code could call the interface whenever the registered interface can be retrieved.  This suggests that an optimal "fracture point" between the Core and the Lazy Loaded code is the Activity.  This is exactly what GWT's "Large scale application development and MVP" documentation suggests (specifically bundling a Place's Activity/View pair behind a RunAsync method).  Obviously, this means that any Lazy Loaded Activity/View pair is NOT available for direct synchronous calls.  Unfortunately, this implication at the end of GWT's "Large scale application development and MVP" documentation invalidates the recommendations in GWT's "GWT Development with Activities and Places" to have all the Views synchronously accessible in the ClientFactory.

The recommended start up infrastructure code when using the Place/Activity/View approach (from "GWT Development with Activities and Places") is:

        ClientFactory clientFactory = GWT.create(ClientFactory.class);
        EventBus eventBus = clientFactory.getEventBus();
        PlaceController placeController = clientFactory.getPlaceController();

        // Start ActivityManager for the main widget with our ActivityMapper
        ActivityMapper activityMapper = new AppActivityMapper(clientFactory);
        ActivityManager activityManager = new ActivityManager(activityMapper, eventBus);
        activityManager.setDisplay(appWidget);

        // Start PlaceHistoryHandler with our PlaceHistoryMapper
        AppPlaceHistoryMapper historyMapper= GWT.create(AppPlaceHistoryMapper.class);
        PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper);
        historyHandler.register(placeController, eventBus, defaultPlace);

With GWT providing these classes:

    PlaceHistoryMapper (interface),
    ActivityMapper (interface),
    ActivityManager (class), &
    PlaceController (class),
    PlaceHistoryHandler (class).

And the App developer providing concrete implementations of these classes/interfaces:

    PlaceHistoryMapper  ->  AppPlaceHistoryMapper, &
    ActivityMapper      ->  AppActivityMapper.

However, the ActivityMapper's "Activity getActivity(Place place)" method expects to return an Activity synchronously (it does not utilize a Call Back mechanism to support asynchronous creation of the Activity), so obviously the "ActivityMapper" needs to be changed/replaced.

Unfortunately, the ActivityManager's "Activity getNextActivity(Place place)" method uses ActivityMapper's "getActivity" method privately, so obviously as a concrete class the "ActivityManager" needs to be replaced (with its mixture of EventBus management, which we don't use, and all its private methods, overriding appeared to be much more work than simply replacing the ActivityManager's functionality).

Unfortunately, the PlaceController's "void goTo(Place place)" method uses ActivityManager's "getNextActivity" method indirectly via the synchronous EventBus, so obviously as a concrete class the "PlaceController" needs to be replaced (with its implicit dependency on ActivityManager, once ActivityManager is replaced it is simpler to replace the PlaceController's functionality - especially when looking at PlaceHistoryHandler - next paragraph).

Unfortunately, both PlaceHistoryHandler's the "void handleCurrentHistory()" and the "void handleHistoryToken(String token)" methods use PlaceController's "goTo" method.  While this at first does not appear to be a problem, there are actually two problems: first the hard coded dependence on PlaceController which is being replaced, and secondly the use of PlaceController's "goTo" method posts the Place's token to the URL (on the assumption that it is called by code to go to a new Place).  If the Place's token is NOT an EXACT match to the token that created the Place (no one has added state to the Place for "Browser Forward button support" or the encoding of the token is not 100% deterministic - My Bad!), then a new History Entry is added to the Browser History Stack/Queue (eliminating all "Forward" History Entries)!  For these reasons the PlaceHistoryHandler needs to be replaced.

Finally, since we don't use the EventBus, and with the PlaceController and the ActivityManager being replaced and not using the EventBus, continuing the assumed need for the EventBus in the Activity(s) seemed dangerous (as developers might assume they could send Place Change related Events, and they might assume that EventBus Registrations would be automatically un-registered by the ActivityManager as documented).  So the Activity interface was replaced and a new AbstractActivity was created.

--------------------------------------------------------------

While the primary problem was to make Activity/View acquisition asynchronous so that Lazy Loading of code could be achieved, the following additional goals are also shown/implemented:

    0) Actual sample code showing both Regular & Lazy Loaded Activity/View(s) and new Start-up code.

    1) Make Token IDs explicitly unique and IDE navigable friendly.

    2) Speed up development by partitioning the code physically so that code not under development is NOT even looked at by GWT.

    3) A Proxying Logger that is both extremely efficient and GWT Client friendly.

    4) Client Cache management using a Versioned URL w/ static path remapping for both common files and development.

    5) The use of a Status "bar" for messages to the User (where a Dialog would probably be irritating).

In addition, an unintentional result is an exposure to my preferred coding style that is the result of three of the last four teams agreement (over the last 10 years).



Goals Detail

Goal 0) Actual Infrastructure implementations & sample code showing the Lazy Loading of Activity/View(s), and new Start-up code.

The fundamentally new concept from the documentation about Place/Activity/View to support the RunAsync mechanism, is the ActivityFactory interface, and specifically its two child interfaces:

    Synchronous, &
    Asynchronous.

To make everything: Place(s), PlaceTokenizer(s), & ActivityFactory(s); work together, a triad of these instances/classes are all tied together via a "PlaceId" (which is a String that identifies the Place from the URL Fragment).  While there is a class (PlaceIdExtractor) that supports automatic PlaceId determination for compatibly named Place (sub-classes) and PlaceTokenizer(s) that are static inner classes of a compatibly named Place (sub-class), there is no automatic support for ActivityFactory(s).  The simplest approach is to use the same "source" (see section - Make Token IDs explicitly unique and IDE navigable friendly) for the PlaceId(s) of the "triad" and to have your Place(s) extend IdPlace, and your PlaceTokenizer(s) extend IdTokenizer.

The 2 interfaces and 5 concrete classes that wired everything together before are replaced by the following 3 classes:

    PlaceRegistry,
    ActivityFactoryRegistry, &
    PlaceActivityManager (and two support classes in the "internal" package).

The function of mapping a 'token' (PlaceId) to and from a Place that was previously handled by "PlaceHistoryMapper" and "AppPlaceHistoryMapper" is now handled by the "PlaceRegistry" and "PlaceActivityManager". 

The function of mapping a Place to an Activity that was previously handled by "ActivityMapper", "AppActivityMapper", and "ActivityManager" is now handled by the "ActivityFactoryRegistry" and "PlaceActivityManager". 

The function of managing the dance between the Place, the display Widget, and the History infrastructure that was previously handled by "PlaceController", "ActivityManager", and "PlaceHistoryHandler" is now handled by the "PlaceActivityManager". 

Initialization of a "new" structured application is split into four actions:

    Registering PlaceTokenizer(s) (see Foundation.java),
    Registering ActivityFactory(s) (see GroupBack.java, GroupFront.java, & Main.java),
    The instantiation of the CommonActivityParameter(ClientFactory), MessageUserSink, & PlaceActivityManager/PlaceChanger (see Main.java), &
    Process the initial URL Fragment/token (see Main.java).

All the PlaceTokenizer(s) are registered in one place (unlike the ActivityFactory(s)).  While it would be possible to register the PlaceTokenizer(s) along side their respective ActivityFactory(s) it was thought that it was better to produce an error like "No Activity Found" (which would be a programming mistake, see section - Speed up development by partitioning the code physically so that code not under development is NOT even looked at by GWT) instead of a warning like "Unrecognized Token" which can be produced by simply putting trash in the URL Fragment/token.
 
A CachedViewSynchronousActivityFactory, Synchronous ActivityFactory(s) would normally be used only when the likelihood of starting there approaches 100%, is registered in Main.java, and Asynchronous ActivityFactory(s) are registered in GroupBack.java & GroupFront.java.  Why they are registered in two places is explained in section - Speed up development by partitioning the code physically so that code not under development is NOT even looked at by GWT.


Goal 1) Make Token IDs explicitly unique and IDE navigable friendly.

In previous applications I have worked with, all the PlaceId(s) / tokens were String constants in a single file.  While this did not ensure that the Strings were unique, it at least had all them in one spot to make it easier to detect collisions.  By making the PlaceId(s) / tokens something that an IDE could find usages for, it allowed a developer to get to the code supporting a view quite easily and with confidence (unlike using Eclipse's lame String search).  While the obvious next step is to use an Enum, they were not supported in the initial GWT releases.  In this application, the uniqueness, and usage search ability is represented by an Enum called "Places".  I will be going back to my 6 existing apps and retrofitting them to this pattern.


Goal 2) Speed up development by partitioning the code physically so that code not under development is NOT even looked at by GWT.

With any large application there is usually a dependency graph between different areas of the application.  The separation mechanism of these areas runs from formal thru convention:

   1) Separate processes w/ formal RPC APIs,
   2) Separate simple compilation units (JARs) with explicit IDE/build dependencies,
   3) Package naming conventions (e.g. client, shared, & server).

Client side GWT code supports options 2 & 3.  Speeding up the development cycle is accomplished by reducing the code that GWT looks at and then reducing the code GWT must load.  To reduce the code that GWT looks at, you have to break the code up into multiple GWT modules (which also equate to Entry Points) and then during development only include those modules actually needed for the area under development.  The sample application consists of 6 source trees (using option 2 above) which represent 4 GWT modules:

    - foundation - always needed
    - groupBack - an arbitrary set of places (Top, Back, Bottom)
    - groupFront - an arbitrary set of places (Left, Top, Right)
    - main - start up code and module selection

By commenting out the "inherits" for "groupBack" from the "Main.gwt.xml" file, GWT will completely ignore the three views registered and built in "groupBack".

In a large application, a developer can save significant time per cycle by removing all the areas not actively under development.  Note: Since during development a parameter is used to indicate the ".gwt.xml" to "launch" other projects split the "main" ".gwt.xml" file into three files: core/common "stuff", production dependencies/inherits, dev/testing dependencies/inherits; this insures that the stripped down dev/test version never gets put into production.  

The main advantage that separation mechanisms 1 & 2 above give, is that they leverage the IDE to keep the developer from accidentally referencing something that they shouldn't.  For a monolithically (no Lazy Loaded code) compiled GWT application it doesn't really matter what we do, but with Lazy Loading, GWT either "assumes" that the code in the "GWT.runAsync( …" block is NOT referenced any place else, OR it checks if it is, and if found any place else simply ignores the "Lazy Loading" request and includes the code in the main JavaScript chunk (this latter behavior is what I "think" GWT does as it would reduce "complaints").  Either result is NOT a happy outcome.  Separation mechanism #2 if carried to the extreme makes it impossible for a developer to even reference inappropriate code.

We need to decide how much of separation mechanism #2 we will do.

When we do separation mechanism #2 we are forced to pick a "package" structure that supports modularization, at least one/JAR.

However, for the areas of code not using separation mechanism #2 (or within the individual compilation units), we need to decide to what level we want to support a development friendly modularization "package" structure.  


Goal 3) A Proxying Logger that is both extremely efficient and GWT Client friendly.

Logging in the Java community is a mess, what with Log4J out initially, then java.util.Logger introduced (but incompatible with Log4J), then Log4J forked into LogBack, etc… Then later SLF4J (Simple Logging Facade for Java) was created to unify all the others.  In addition, I have always been disappointed that logging was a fundamental aspect of the language (similar to asserts) that specifically eliminated the parameter object construction if the logging level was disabled.  Before SLF4J was introduced a friend, Jason Osgood, showed me his Flogger open source project which introduced a new signature model, instead of logger.warn( … ), he proposed logger.warn().log(…); this allowed for the ability to pass around different levels of a logger between objects.  I saw this and immediately saw the opportunity to expand the number of method signatures, on the "inner logger" interface, e.g. warn(), dramatically so that object creation is deferred until AFTER the check to determine if the log level is enabled.  So I created the LiteSoftLogger that does not create an object for logging unless the level is enabled, and for efficiency reason does not allow dynamic level changes, so it actually only checks the level the first time it is called (it then replaces itself with either a no-op version if the level is disabled, or an object creating forwarding version).  Later a pre-configured version was added so that logging could occur before the logger was even configured.

The LiteSoftLogger, unlike: Log4J, java.util.Logger, LogBack, and even SLF4J; does not use any IO related files, and is thus GWT friendly.  This allows shared code (that is used in BOTH the client side and the server side) to use a common logging interface.  In the LiteSoft space there are two current "back-ends" for the LiteSoftLogger: GWT Client side - user initiated pop-up browser window logger output, and a Server side Log4J adapter.


Goal 4) Client Cache management using a Versioned URL w/ static path remapping for both common files and development.

Until I saw the code here, I had never seen a GWT webapp that did not "run" from the GWT generated directory.  We are currently "running" the webapp from the "webapp root", and launching the GWT code from the GWT generated directory.  This approach has two benefits, first, that from the user's URL perspective the GWT generated directory/path does not exist, and second, that the "stuff" GWT drops into the GWT generated directory/path can easily be excluded from source control.  However, it does mean that all the non-GWT managed resources, e.g.: images, help files, and the CSS file; are directly in the "root" folder, and hence are the core problem when it comes to Client side cache management.

The sample app uses an approach of putting versioning directly in the user's URL.  It accomplishes this in two ways, first by having GWT generate its files directly into the versioned directory, and second by dividing all the files delivered to the browser into one of the following groups:

    A) *.nocache.js - the GWT bootstrap JavaScript - which is never cached and automatically rolls the user URL version forward.
    B) Non-Versioned "root" "welcome" HTML file - this file simply forwards to a versioned URL - the caching state of this file is unimportant.
    C) Versioned directory files: Minimal set of static files to support initial launch (including when JavaScript is disabled) and the GWT generated files.
    D) Common files that technically only are guaranteed to work for the latest version.

A tomcat filter "VersionedStaticContentFilter.jar" automatically sets files in group 'A' to cache "never", and all other files to cache "forever".  It also automatically remaps all request URLs of the form "v?/common/...", where the '?' is a number, to "common/..." thus allowing "common" to be a peer directory of the versioned directory on the server, while "common" appearing to be a child directory of the versioned directory to the Browser.

Historically, when an application has been modularized (using the separation mechanism #2), you end up with multiple directories of web "resources".  In development mode, GWT is more than happy to copy this files to your "working" webapp directory.  Unfortunately, I have seen UI developers change the file in the "working" webapp directory to quickly see the changes and then forget to copy the changes back to the original directory.  The "VersionedStaticContentFilter.jar" tomcat filter supports this by allowing a command line set of remap directories so that the files do not need to be copied until production packaging.

In addition, the "VersionedStaticContentFilter.jar" tomcat filter has a command line dev switch that turns off all caching!

I believe, that an enhanced version of the "VersionedStaticContentFilter.jar" tomcat filter could be developed that, in conjunction with "never" caching the "launching" JSP(s), would fool the Browser into seeing all subordinate (non-GWT generated) resources as versioned URLs.  It would require that either the filter or the individual JSP(s) would need to set them to "never" cache, and that a "current" version would need to be injected into the direct resources (e.g. CSS file references), and as a JavaScript variable that the GWT code could pick up and utilize in ALL its URL resource requests!  ********  This latter might be a big job to get right ********!    


Goal 5) The use of a Status "bar" for messages to the User (where a Dialog would probably be irritating).

In the majority of the Enterprise oriented applications I have been involved with, the applications had a status bar.  This was used to display information that did NOT require the user to acknowledge (hence not needing a Dialog).  This information when presented was color coded to indicate its type: Black on White = Informational, Black on Yellow = Warnings, and Black on Red = Error.

An example of an "Informational" message would be when the user has saved something but the view shifts back to the "home" view, then the status would indicate that: "blah blah blah was saved."

A "Warning" message might be something like: "No Screen exists for 'Homie'!"

An "Error" message might be something like: "No Activity found for PlaceId: Back"  (these kinds of "Error"s would normally on surface hopefully during development)


-----------------
George























Commits for litesoft/trunk/Documents/GWTMultiModuleRunAsynExampleApp.txt

Diff revisions: vs.
Revision Author Commited Message
561 GeorgeS picture GeorgeS Thu 13 Oct, 2011 22:28:36 +0000