|By Hal Helms||
|November 1, 2001 12:00 AM EST||
Someone once said that people should never see how laws or sausages are made. I think I can safely add technical standards to that list. There have been passionate arguments and some painful missteps along the way, but that's all eclipsed as people begin discovering the new power and ease-of-use in Fusebox 3.0.
What's New in Fusebox 3.0
Here's a rundown of some of the key features:
- A nested model for communication between circuits created to make reuse and distributed development easier
- A nested layout model that opens up possibilities for highly dynamic, flexible layouts
- XML-based Fusedoc providing both PDL (Program Definition Language) and documentation for fuses
- A stable set of key files that forms a Fusebox skeleton, making the learning process easier for people new to Fusebox and making it much easier to generate a new Fusebox application
- Exit fuseactions (XFAs) for greater reusability of fuses and circuits
- An API that exposes key variables within a Fusebox structure defined by the key Fusebox file
The core Fusebox files begin with an FBX_ prefix. A sample application is assumed with the circuit structure shown in Figure 1.
The component Fusebox files are:
This file is optional and is used to set variables that are needed by the application. Each circuit may have its own FBX_Settings.cfm file. If a fuseaction such as grandchild.main is resolved to home/parent/ child/grandchild.main, FBX_ Settings.cfm will be called in each directory in order. This allows you to set general variables at higher levels and override them, if needed, in child directories. This file replaces myGlobals.cfm, app_ Globals.cfm, and app_Locals.cfm.
Required in the home circuit, FBX_ Circuits.cfm maps circuit aliases to physical paths. In the sample application shown, the contents of FBX_Circuits.cfm are:
Fusebox.Circuits.home = 'Grandparent';
Fusebox.Circuits.parent = 'Grandparent/Parent';
Fusebox.Circuits.Child = 'Grandparent/Parent/Child';
Fusebox.Circuits.grandchild = 'Grandparent/Parent/Child/Grandchild';
The circuit alias is a key within a structure called Circuits, with the reserved structure Fusebox. The circuit alias doesn't have to be the same name as the physical directory, as shown by aliasing the top directory (Grandparent) as home. This allows for directories at different levels to have the same name.
This file is placed in every circuit that handles fuseactions; FBX_Switch.cfm is a <cfswitch> statement with <cfcase>s for every fuseaction the individual circuit is to handle.
Required in any circuit that implements a separate layout file, FBX_Layouts.cfm is responsible for setting the variables, Fusebox.layoutDir and Fusebox.layoutFile. The layout file pointed to in Fusebox.layoutFile must, at a minimum, output Fusebox.layout. Conditional processing is possible within FBX_Layouts.cfm. If, for example, a child sets a Boolean variable called QueryReturnedRecords, a parent might implement this simple logic in FBX_Layouts.cfm:
Used if the ColdFusion server version is below version 5.0, this file is the widely popular <cf_bodycontent> tag.
Of the files FBX_Fusebox30_CF50.cfm, FBX_Fusebox30_CF45.cfm, and FBX_Fusebox30_CF40.cfm, only one will be called in index.cfm, depending on which version of ColdFusion server is running. The index.cfm file should have this code in it:
<cfif Val(ListGetAt(Server.ColdFusion.ProductVersion, 1)) LT "5">
<cfif Val(ListGetAt(Server.ColdFusion.ProductVersion, 2)) is "5">
<cfinclude template="fbx_fusebox30 _CF45.cfm">
<cfinclude template="fbx_fusebox30_ CF40.cfm">
Note: If you create circuit applications, the only file that's absolutely required for a circuit that only runs beneath other circuits is the fbx_switch.cfm file. The other files are needed only if you have special settings to set and special layouts to handle. Circuits are defined only in the home circuit. Of course, you do have to supply your own fuseactions and fuses; we had to leave some of the work for you, after all!
In addition to the core Fusebox files, DefaultLayout.cfm is provided. This file simply outputs Fusebox.layout. This is a useful file when you don't care about a given circuit adding any layouts and simply want it to display the layout. Note, however, that while the fuseaction path must have one layout file, you don't need any files in circuits that don't have any special layout requirements. In the "family" application whose directory structure is shown in Figure 1, a request for grandchild.main might, for example, have layout handled in only one of the circuits involved. In such a case the other circuits wouldn't require an FBX_Layouts.cfm file nor the use of DefaultLayout.cfm.
Reserved Variables: The Fusebox 3.0 API
There are two complex variable structures. The first, the FB_ structure, is used for internal calculations by the FBX_Fusebox_CFxx.cfm file. You should neither read nor write to this structure since it will immediately render your code non-Fusebox 3.0-compliant and may well break your application.
The second reserved variable structure is the public "API" called Fusebox (see Table 1). Again, you shouldn't make any changes to this API. You'll find some variables within this structure very useful. You can use these public variables anywhere within your code, as any changes to the Fusebox standard will support these variables going forward, even if the underlying code that calculates these variables is modified.
Core Fusebox 3.0 Concepts
With the new terminology, API, and required files under our belts, let's turn our attention to the latest additions to the "core Fusebox" methodology that makes its appearance in Fusebox 3.0. These are exit fuseactions (XFAs), Fusedocs, and nested circuits.
XFAs address issues of severability and reuse; Fusedocs address code documentation, especially in a distributed developer environment; nested circuits address inheritance, layouts, and exception handling. Although these are largely separate concepts, they do weave into the overall Fusebox scheme and are absolutely essential toward realizing the advantages that Fusebox 3 can deliver.
Exit Fuseactions (XFAs)
The first of the new concepts for Fusebox 3.0 is exit fuseactions. XFAs are the identified exit points of a particular fuse. In Fusebox the meaning of an exit point is not "Where do we go next?" (we always go back to the fusebox!), but rather, "What fuseaction are we going to take upon our return to the fusebox?"
For example, take the case where a user fills out a form to win the $1 million we're giving away. The user begins by getting an exciting, unexpected e-mail from us (really, it's not spam!) on which appears a link (see Figure 2):
<a href="http://scamsRus.com/index.cfm? fuseaction=showContestForm">Click here to win a million dollars</a>
When the user submits that form, we want to store the information to a database while the user expectantly awaits a celebrity spokesperson's arrival with a substantial cashier's check. Regardless of what the spokesperson does, the form had only one possible way to return to the fusebox - by the user clicking the OK button. We might assign an exit fuseaction of "saveToDatabase" to that exit point, and represent it like this:
The problem with such a fuseaction is that it's a hard reference. To reuse the fuse - either in a new application or in a new context in the same application - I need to either change code or introduce conditional logic into the fuse. Neither path is optimal.
A much better way to handle our sample case is to identify the exit points of a fuse - those paths out of the fuse - and to assign variable names to them. The fuse then becomes completely independent of the application or context in which it operates, allowing it to be easily reused. The form code looks like this:
The value of XFA.submitForm is then set (by the architect) in the new FBX_ Switch.cfm file.
<cfset XFA.submitForm = "saveToDatabase">
Introducing XML Fusedocs
As programmers, we know that we should document our code, but we often create little or no documentation or, at best, documentation is done afterwards. At some firms documentation is taken much more seriously, but often the reason for that documentation is not conveyed. Without knowing why we should do something, how we should do it will never be clear. About the only thing that everyone seems to agree on is that documentation is something that's good for you, like castor oil. That was the old way.
Fusedoc is different. It's not about documenting your code; it's about coding your documentation. The Fusedoc process makes the application architect responsible for documenting the application before coding even starts. Fusedoc supplies all the information the coder needs to complete the fuse.
Fusedoc details exactly what the fuse is expected to do, and the variables and resources it uses and produces (inputs and outputs). It takes the form of a comment at the top of the fuse file.
<fusedoc fuse="fuse_name" version="2.0" language="ColdFusion">
fusedoc elements go hereŠ
In Fusebox 2.0, Fusedocs used a proprietary format that I developed. Starting with Fusebox 3.0, they're now XML-based. If you're familiar with XML, reading a Fusedoc is simple. If you're new to XML, you may find the Fusedoc to be a great confidence builder toward working more with XML.
We're trying to provide enough information so that a competent programmer, who knows nothing of the application nor of the underlying database, can write the code. It's sometimes referred to as programming by contract because the comments provided form a sort of "work order" that tells the coder exactly what to do. The "contract" promises the programmer, "You fulfill this work order and you will be done with the code. You have no responsibility beyond what the document tells you."
With Fusebox 3, Fusedoc goes from a proprietary symbology to the open standard of XML. There's a document type definition (DTD) available at www.halhelms.com that provides the formal specification for Fusedoc. To give you a taste of what one looks like, Listing 1 provides a complete Fusedoc for a fuse, act_ValidateLogin.cfm.
More information on Fusedocs and Fusebox 3.0 is available at www.halhelms. com.
Nesting Circuits and Layouts
In Fusebox 2, integration between separate fuseboxes consisted mainly of "tricks" for sending a browser from one insular fusebox to another. But the true goal is a "drag-and-drop" type of functionality, in which a fusebox consists of one of more circuits (a file directory), all of which can access each other without direct dependence on the underlying directory structure and that, when done, can be dragged-and-dropped to another fusebox, requiring only a few small settings to be changed to the definition of the circuits.
If this type of nesting is combined with some sort of a standard with respect to how a fusebox operates within its own framework, we're able to write large amounts of reusable code. If we continue to adhere strictly to a standard for doing that, like Fusebox 3.0, then people can write entire applications that can be stamped as "Fusebox 3.0-compliant" and then dropped into our own code, saving time and money.
Fusebox 3.0 has its own concept of inheritance that, while very different from object-oriented languages, allows children to inherit properties from their parent, grandparent, and so on. Each of these circuits can be "dragged-and-dropped" into a parent circuit, resembling those wooden Russian dolls in which one doll contains another one. This pattern continues for as long as the artistry of the maker can sustain itself. In Fusebox 3.0 the grandparent circuit may contain one or more parent circuits that may contain one or more child circuits, and so on.
Each circuit, from the top or home circuit and continuing down each intervening circuit to the target circuit, has a settings file called FBX_Settings.cfm. The more global an item in a settings file is, the "higher up" the circuits tree that item would be loaded. So if your application accesses a data source called customers, you may want to set Request.DSN in the home circuit.
Suppose you dropped in a shopping cart circuit that needs a products data source. It might set one as part of its own settings file since that DSN may only be needed by that circuit or one of its child circuits. Of course you, as the architect of your program, can determine whether you want to force it to have a certain value by using cfset or whether you want it to inherit a value from a higher-level circuit - if such a value has already been set - by using a cfparam. In earlier versions of Fusebox, you included the app_globals.cfm once and then used app_locals.cfm for all second-tier circuits. In Fusebox 3.0 there's no hard meaning to "global" or "local" - anything set in a circuit is available to all circuits nested beneath that circuit. Again, remember the Russian doll.
Fusebox automatically reads in any FBX_Settings.cfm files from all circuits in the fuseaction path. Once the target circuit is reached, FBX_Switch.cfm is called, executing the target fuseaction within that circuit. This concept has not changed from Fusebox 1.0.
The concept of nested layouts is a very exciting addition. Having walked "down" the fuseaction circuit tree and executed the fuseaction, Fusebox now walks "up" the tree from the bottom "target" circuit where the fuseaction was found, up through each parent circuit up to the home circuit. Each of those circuits, in order, will have the opportunity to apply its own layout as determined by the FBX_Layouts file, just as you might put a Russian doll back together - each of the larger dolls taking what they got from the smaller doll and wrapping their own wrapper around it. While you can make use of every circuit in the fuseaction path, you don't need to. Figure 3 is a teaching example that shows the use of layouts at each circuit in the fuseaction path.
Migrating from Fusebox 2 to Fusebox 3
The learning curve for FB3 has been made easy due primarily to the standardization of the underlying core Fusebox code combined with a standardization in the naming convention. For these same reasons your migration path from FB2 to FB3 will be fairly easy, especially after you've achieved your first application with the new standard.
What about some of the underlying files you've dealt with before? Well, app_locals.cfm and app_globals.cfm are now combined into a single file called FBX_Settings.cfm. Application.cfm is still not a required file since the functionality you might put there can be put into FBX_Settings.cfm as well.
If you were a fan of Steve Nelson's <cf_bodycontent> custom tag for "wrapping" layout with common design elements, that functionality is still there too. Now, though, you don't need a "header" and a "footer" file, rather you can define actual layout files that include both header and footer elements in a way that's fairly transparent. Again, the "frozen" core fusebox file will pick up your layout files as well.
In Fusebox 2, index.cfm (or the default document) had all the core functionality of Fusebox written into it. In Fusebox 3.0 a new file, FBX_Fusebox30_CFxx.cfm - where xx is a version that's appropriate for the version of the ColdFusion application server your machine is running - takes its place.
Circuits are a new idea for the standard, one that entirely replaces Fusebox 2's ad hoc "dot-dot-slash" method of making one fusebox talk to another. With Fusebox 2 a fuseaction usually had a verb-noun phrase syntax, such as addItem ToCart. In Fusebox 3, fuseactions are written with a circuit_ alias.fuseaction syntax. To solve the problem of circuits on different levels having the same name, an FBX_Circuits.cfm file has been introduced. It maps individual circuits to an alias of your choosing. Nesting/inheritance in Fusebox 3.0 is done for you automatically by the core fusebox file, FBX_Fusebox_CFxx.cfm, using the FBX_ Circuits.cfm file's mappings. Assume that we have an application with the circuit structure shown in Figure 4.
Assume that a fuse within the Granddaughter circuit executes this code:
The variable, self, is defined in the home circuit's FBX_ Settings.cfm as index.cfm (or default file) of the home circuit - Grandmother, in our case. In addition the variable XFA.continue is defined within FBX_Switch.cfm as Grandson.sayHello. All of this is under the control of the application architect.
All action returns to the home circuit's default file. This file includes the version of FBX_Fusebox30_CFxx.cfm that's appropriate for the ColdFusion server that's running. Within this file Fusebox translates the fuseaction, Grandson.sayHello, to the fuseaction path, in effect saying, "You said Grandson.sayHello, but I see from FBX_Circuits.cfm that what you really mean is Grandmother/Father/Son/Grandson.sayHello. Let me execute that for you." All that's required from you is that you properly map the circuits in FBX_Circuits.cfm, a very easy matter, and use the compound circuit_alias.fuseaction syntax.
Fusebox 3.0 represents a major landmark in the Fusebox community's approach to creating a methodology that consistently produces predictable success in Web application development, but it is just that - a landmark, a waystation in an ongoing journey. Fusebox began with ColdFusion and is being adopted by developers working in PHP, ASP, and JSP.
Where do we go from here? Much work remains to be done. With Fusebox 3.0 we've achieved a platform on which we can build even more firmly. In the coming months new work will begin on making ColdFusion even better. We hope that you'll be a part of this venture.
How can you be involved? First, join the Fusebox community officially by signing up at www.fusebox.org. Next, join the Fusebox mailing list; a signup form is available at www.halhelms.com. If you'd like to participate in making the future of Fusebox, sign up for the Fusebox steering mailing list by sending an e-mail to [email protected].
To learn more about being a contract "Fusecoder," check out Steve Nelson's Web site at www.secretagents.com. Lee Borkman is heading up an open-source effort to make wireframes even better; information is available at www.bjork.net. Jeff Peters has some wonderful tools available for Fusecoders at www.grokfusebox.com. John Quarto-vonTivadar has articles available at www.grokdotcom.com (free newsletter signup) and www.johnquarto.com. Finally, you may want to sign up for my free Occasional Newsletter at www.halhelms.com..
Alan Kay, the creator of the language Smalltalk, once said, "The best way to predict the future is to invent it." Let's get busy.
Too often with compelling new technologies market participants become overly enamored with that attractiveness of the technology and neglect underlying business drivers. This tendency, what some call the “newest shiny object syndrome,” is understandable given that virtually all of us are heavily engaged in technology. But it is also mistaken. Without concrete business cases driving its deployment, IoT, like many other technologies before it, will fade into obscurity.
Aug. 30, 2015 10:00 PM EDT Reads: 347
While many app developers are comfortable building apps for the smartphone, there is a whole new world out there. In his session at @ThingsExpo, Narayan Sainaney, Co-founder and CTO of Mojio, will discuss how the business case for connected car apps is growing and, with open platform companies having already done the heavy lifting, there really is no barrier to entry.
Aug. 30, 2015 05:00 PM EDT Reads: 136
As more intelligent IoT applications shift into gear, they’re merging into the ever-increasing traffic flow of the Internet. It won’t be long before we experience bottlenecks, as IoT traffic peaks during rush hours. Organizations that are unprepared will find themselves by the side of the road unable to cross back into the fast lane. As billions of new devices begin to communicate and exchange data – will your infrastructure be scalable enough to handle this new interconnected world?
Aug. 30, 2015 04:00 PM EDT Reads: 154
SYS-CON Events announced today that Micron Technology, Inc., a global leader in advanced semiconductor systems, will exhibit at the 17th International Cloud Expo®, which will take place on November 3–5, 2015, at the Santa Clara Convention Center in Santa Clara, CA. Micron’s broad portfolio of high-performance memory technologies – including DRAM, NAND and NOR Flash – is the basis for solid state drives, modules, multichip packages and other system solutions. Backed by more than 35 years of technology leadership, Micron's memory solutions enable the world's most innovative computing, consumer,...
Aug. 30, 2015 01:30 PM EDT Reads: 222
SYS-CON Events announced today that Pythian, a global IT services company specializing in helping companies leverage disruptive technologies to optimize revenue-generating systems, has been named “Bronze Sponsor” of SYS-CON's 17th Cloud Expo, which will take place on November 3–5, 2015, at the Santa Clara Convention Center in Santa Clara, CA. Founded in 1997, Pythian is a global IT services company that helps companies compete by adopting disruptive technologies such as cloud, Big Data, advanced analytics, and DevOps to advance innovation and increase agility. Specializing in designing, imple...
Aug. 30, 2015 01:00 PM EDT Reads: 286
SYS-CON Events announced today that HPM Networks will exhibit at the 17th International Cloud Expo®, which will take place on November 3–5, 2015, at the Santa Clara Convention Center in Santa Clara, CA. For 20 years, HPM Networks has been integrating technology solutions that solve complex business challenges. HPM Networks has designed solutions for both SMB and enterprise customers throughout the San Francisco Bay Area.
Aug. 30, 2015 10:30 AM EDT Reads: 873
Consumer IoT applications provide data about the user that just doesn’t exist in traditional PC or mobile web applications. This rich data, or “context,” enables the highly personalized consumer experiences that characterize many consumer IoT apps. This same data is also providing brands with unprecedented insight into how their connected products are being used, while, at the same time, powering highly targeted engagement and marketing opportunities. In his session at @ThingsExpo, Nathan Treloar, President and COO of Bebaio, will explore examples of brands transforming their businesses by t...
Aug. 30, 2015 10:15 AM EDT Reads: 227
Through WebRTC, audio and video communications are being embedded more easily than ever into applications, helping carriers, enterprises and independent software vendors deliver greater functionality to their end users. With today’s business world increasingly focused on outcomes, users’ growing calls for ease of use, and businesses craving smarter, tighter integration, what’s the next step in delivering a richer, more immersive experience? That richer, more fully integrated experience comes about through a Communications Platform as a Service which allows for messaging, screen sharing, video...
Aug. 30, 2015 09:15 AM EDT Reads: 619
With the proliferation of connected devices underpinning new Internet of Things systems, Brandon Schulz, Director of Luxoft IoT – Retail, will be looking at the transformation of the retail customer experience in brick and mortar stores in his session at @ThingsExpo. Questions he will address include: Will beacons drop to the wayside like QR codes, or be a proximity-based profit driver? How will the customer experience change in stores of all types when everything can be instrumented and analyzed? As an area of investment, how might a retail company move towards an innovation methodolo...
Aug. 30, 2015 09:15 AM EDT Reads: 443
The Internet of Things (IoT) is about the digitization of physical assets including sensors, devices, machines, gateways, and the network. It creates possibilities for significant value creation and new revenue generating business models via data democratization and ubiquitous analytics across IoT networks. The explosion of data in all forms in IoT requires a more robust and broader lens in order to enable smarter timely actions and better outcomes. Business operations become the key driver of IoT applications and projects. Business operations, IT, and data scientists need advanced analytics t...
Aug. 30, 2015 08:30 AM EDT Reads: 395
A producer of the first smartphones and tablets, presenter Lee M. Williams will talk about how he is now applying his experience in mobile technology to the design and development of the next generation of Environmental and Sustainability Services at ETwater. In his session at @ThingsExpo, Lee Williams, COO of ETwater, will talk about how he is now applying his experience in mobile technology to the design and development of the next generation of Environmental and Sustainability Services at ETwater.
Aug. 30, 2015 07:00 AM EDT Reads: 123
WebRTC has had a real tough three or four years, and so have those working with it. Only a few short years ago, the development world were excited about WebRTC and proclaiming how awesome it was. You might have played with the technology a couple of years ago, only to find the extra infrastructure requirements were painful to implement and poorly documented. This probably left a bitter taste in your mouth, especially when things went wrong.
Aug. 30, 2015 01:00 AM EDT Reads: 439
SYS-CON Events announced today that IceWarp will exhibit at the 17th International Cloud Expo®, which will take place on November 3–5, 2015, at the Santa Clara Convention Center in Santa Clara, CA. IceWarp, the leader of cloud and on-premise messaging, delivers secured email, chat, documents, conferencing and collaboration to today's mobile workforce, all in one unified interface
Aug. 30, 2015 01:00 AM EDT Reads: 395
As more and more data is generated from a variety of connected devices, the need to get insights from this data and predict future behavior and trends is increasingly essential for businesses. Real-time stream processing is needed in a variety of different industries such as Manufacturing, Oil and Gas, Automobile, Finance, Online Retail, Smart Grids, and Healthcare. Azure Stream Analytics is a fully managed distributed stream computation service that provides low latency, scalable processing of streaming data in the cloud with an enterprise grade SLA. It features built-in integration with Azur...
Aug. 28, 2015 07:45 PM EDT Reads: 211
Akana has announced the availability of the new Akana Healthcare Solution. The API-driven solution helps healthcare organizations accelerate their transition to being secure, digitally interoperable businesses. It leverages the Health Level Seven International Fast Healthcare Interoperability Resources (HL7 FHIR) standard to enable broader business use of medical data. Akana developed the Healthcare Solution in response to healthcare businesses that want to increase electronic, multi-device access to health records while reducing operating costs and complying with government regulations.
Aug. 26, 2015 07:00 AM EDT Reads: 133
For IoT to grow as quickly as analyst firms’ project, a lot is going to fall on developers to quickly bring applications to market. But the lack of a standard development platform threatens to slow growth and make application development more time consuming and costly, much like we’ve seen in the mobile space. In his session at @ThingsExpo, Mike Weiner, Product Manager of the Omega DevCloud with KORE Telematics Inc., discussed the evolving requirements for developers as IoT matures and conducted a live demonstration of how quickly application development can happen when the need to comply wit...
Aug. 2, 2015 11:15 AM EDT Reads: 556
The Internet of Everything (IoE) brings together people, process, data and things to make networked connections more relevant and valuable than ever before – transforming information into knowledge and knowledge into wisdom. IoE creates new capabilities, richer experiences, and unprecedented opportunities to improve business and government operations, decision making and mission support capabilities.
Aug. 1, 2015 10:00 AM EDT Reads: 479
Explosive growth in connected devices. Enormous amounts of data for collection and analysis. Critical use of data for split-second decision making and actionable information. All three are factors in making the Internet of Things a reality. Yet, any one factor would have an IT organization pondering its infrastructure strategy. How should your organization enhance its IT framework to enable an Internet of Things implementation? In his session at @ThingsExpo, James Kirkland, Red Hat's Chief Architect for the Internet of Things and Intelligent Systems, described how to revolutionize your archit...
Jul. 30, 2015 07:30 PM EDT Reads: 1,564
MuleSoft has announced the findings of its 2015 Connectivity Benchmark Report on the adoption and business impact of APIs. The findings suggest traditional businesses are quickly evolving into "composable enterprises" built out of hundreds of connected software services, applications and devices. Most are embracing the Internet of Things (IoT) and microservices technologies like Docker. A majority are integrating wearables, like smart watches, and more than half plan to generate revenue with APIs within the next year.
Jul. 30, 2015 02:30 PM EDT Reads: 281
Growth hacking is common for startups to make unheard-of progress in building their business. Career Hacks can help Geek Girls and those who support them (yes, that's you too, Dad!) to excel in this typically male-dominated world. Get ready to learn the facts: Is there a bias against women in the tech / developer communities? Why are women 50% of the workforce, but hold only 24% of the STEM or IT positions? Some beginnings of what to do about it! In her Opening Keynote at 16th Cloud Expo, Sandy Carter, IBM General Manager Cloud Ecosystem and Developers, and a Social Business Evangelist, d...
Jul. 30, 2015 12:00 PM EDT Reads: 2,228