Seven simple reasons to use AppFuse

Seven simple reasons to use AppFuse

Learn about -- and be more productive with -- Java open source tools


Level: Introductory

Matt Raible (mraible@virtuas.com), Open Source Practice Leader, Virtuas Open Source Solutions

08 Aug 2006

Getting started with open source tools for the Java™ platform such as Spring, Hibernate, or MySQL can be difficult. Throw in Ant or Maven, a little Ajax with DWR, and a Web framework -- say, JSF -- and you're up to your eyeballs just trying to configure your application. AppFuse removes the pain of integrating open source projects. It also makes testing a first-class citizen, allows you to generate your entire UI from database tables, and supports Web services with XFire. Furthermore, AppFuse's community is healthy and happy -- and one of the few places where users of different Web frameworks actually get along.
<script language="JavaScript" type="text/javascript"> </script>

AppFuse is an open source project and application that uses open source tools built on the Java platform to help you develop Web applications quickly and efficiently. I originally developed it to eliminate the ramp-up time I often found when building new Web applications for customers. At its core, AppFuse is a project skeleton, similar to the one that's created by your IDE when you click through a wizard to create a new Web project. When you create a project with AppFuse, it prompts you for the open source frameworks you'd like to use and then creates your project. It uses Ant to drive testing, code generation, compilation, and deployment. It provides your directory and package structure, as well as the libraries you'll need to develop a Java language-based Web application.

Unlike the products of most "new project" wizards, AppFuse-created projects contain a number of classes and files from the very beginning. These files are used to implement features, but they also serve as examples for you when you're developing your application. By using AppFuse to start new projects, it's possible to eliminate the usual first week or two of development time. You don't have to worry about configuring open source frameworks together because that's already done for you. Your project is preconfigured to talk to a database, deploy in an application server, and authenticate users. There's no need for you to implement security features because they're already integrated.

When I first developed AppFuse, it only supported Struts and Hibernate. Over the years, I've found better Web frameworks than Struts, so I added options for them as well. Today, AppFuse supports Hibernate or iBATIS as persistence frameworks. For the Web framework, you can use JavaServer Faces (JSF), Spring MVC, Struts, Tapestry, or WebWork.

AppFuse comes out of the box with features that many applications need, including:

  • Authentication and authorization
  • User management
  • Remember Me (which saves your login information so you don't have to log in every time)
  • Password reminder
  • Signup and registration
  • SSL switching
  • E-mail
  • URL rewriting
  • Skinability
  • Page decoration
  • Templated layout
  • File upload

This out-of-the-box functionality is one of the main things that separate AppFuse from the other CRUD generation frameworks (from create, retrieve, update and delete), including Ruby on Rails, Trails, and Grails. The aforementioned frameworks, as well as AppFuse, allow you to generate master/detail pages from database tables or existing model objects.

Figure 1 illustrates the conceptual design of a typical AppFuse application:


Figure 1. Typical AppFuse application
Typical AppFuse application

Listing 1 illustrates the command-line interaction you'd go through to create a project named devworks, along with the resulting output. This project uses WebWork as its Web framework (see the Resources section below for a link).


Listing 1. Creating a new project with AppFuse
alotta:~/dev/appfuse mraible$ ant new
Buildfile: build.xml

clean:
     [echo] Cleaning build and distribution directories

init:

new:
     [echo] 
     [echo] +-------------------------------------------------------------+
     [echo] |    -- Welcome to the AppFuse New Application Wizard! --     |
     [echo] |                                                             |
     [echo] | To create a new application, please answer the following    |
     [echo] | questions.                                                  |
     [echo] +-------------------------------------------------------------+

    [input] What would you like to name your application [myapp]?
devworks
    [input] What would you like to name your database [mydb]?
devworks
    [input] What package name would you like to use [org.appfuse]?
com.ibm
    [input] What web framework would you like to use [webwork,tapestry,spring,js
f,struts]?
webwork
     [echo] Creating new application named 'devworks'...
     [copy] Copying 359 files to /Users/mraible/Work/devworks
     [copy] Copying 181 files to /Users/mraible/Work/devworks/extras
     [copy] Copying 1 file to /Users/mraible/Work/devworks
     [copy] Copying 1 file to /Users/mraible/Work/devworks

install:
     [echo] Copying WebWork JARs to ../../lib
     [copy] Copying 6 files to /Users/mraible/Work/devworks/lib
     [echo] Adding WebWork entries to ../../lib.properties
     [echo] Adding WebWork classpath entries
     [echo] Removing Struts-specific JARs
   [delete] Deleting directory /Users/mraible/Work/devworks/lib/struts-1.2.9
   [delete] Deleting directory /Users/mraible/Work/devworks/lib/strutstest-2.1.3
     [echo] Deleting struts_form.xdt for XDoclet
   [delete] Deleting directory /Users/mraible/Work/devworks/metadata/templates
     [echo] Deleting Struts merge-files in metadata/web
   [delete] Deleting 7 files from /Users/mraible/Work/devworks/metadata/web
     [echo] Deleting unused Tag Libraries and Utilities
   [delete] Deleting 2 files from /Users/mraible/Work/devworks/src/web/org/appfu
se/webapp
     [echo] Modifying appgen for WebWork
     [copy] Copying 12 files to /Users/mraible/Work/devworks/extras/appgen
     [echo] Replacing source and test files
   [delete] Deleting directory /Users/mraible/Work/devworks/src/web/org/appfuse/
webapp/form
   [delete] Deleting directory /Users/mraible/Work/devworks/src/web/org/appfuse/
webapp/action
     [copy] Copying 13 files to /Users/mraible/Work/devworks/src
   [delete] Deleting directory /Users/mraible/Work/devworks/test/web/org/appfuse
/webapp/form
   [delete] Deleting directory /Users/mraible/Work/devworks/test/web/org/appfuse
/webapp/action
     [copy] Copying 5 files to /Users/mraible/Work/devworks/test
     [echo] Replacing web files (images, scripts, JSPs, etc.)
   [delete] Deleting 1 files from /Users/mraible/Work/devworks/web/scripts
     [copy] Copying 34 files to /Users/mraible/Work/devworks/web
   [delete] Deleting: /Users/mraible/Work/devworks/web/WEB-INF/validator-rules-c
ustom.xml
     [echo] Modifying Eclipse .classpath file
     [echo] Refactoring build.xml
     [echo] ----------------------------------------------
     [echo] NOTE: It's recommended you delete extras/webwork as you shouldn't ne
ed it anymore.
     [echo] ----------------------------------------------
     [echo] Repackaging info written to rename.log
     [echo] 
     [echo] +-------------------------------------------------------------+
     [echo] |           -- Application created successfully! --           |
     [echo] |                                                             |
     [echo] | Now you should be able to cd to your application and run:   |
     [echo] | > ant setup test-all                                        |
     [echo] +-------------------------------------------------------------+

BUILD SUCCESSFUL
Total time: 15 seconds

Why WebWork?
The Struts community has recently embraced WebWork and the combination has resulted in a great new Web framework for the Java platform: Struts 2. Sure, Spring MVC is a nice request-based framework, but it does not support JSF the way Struts 2 does. Component-based frameworks such as JSF and Tapestry are nice, but I've found WebWork to be more intuitive and easier to work with. (See Resources for more on Struts 2 and JSF.)

After you create a project, you wind up with a directory structure similar to the one illustrated in Figure 2. Eclipse and Intellij IDEA project files are created as part of this process.


Figure 2. Project directory structure
Project directory structure

This directory structure is very close to what Sun recommends for Java 2 Platform, Enterprise Edition (J2EE) Web applications. In version 2.0 of AppFuse, this structure will change to match the standard directory layout for the Apache Maven project (see Resources for links to both directory guidelines). AppFuse will also move from Ant to Maven 2 to obtain capabilities for transitive dependency downloading and support for generating IDE project files. The current Ant-based system requires that committers maintain project files, whereas Maven 2 can generate IDEA, Eclipse, and NetBeans project files simply by using the project's pom.xml file. (This file, located in your project's root directory, is the main component you need to build an application with Maven. It's very similar to the build.xml file you use with Ant.)

Now that you have a sense of what AppFuse is, in the remainder of this article, I'll cover seven simple reasons to use it. Even if you choose not to use it to start your projects, you will see that it provides much of the boilerplate code that can be used in Java language-based Web applications. Because it's Apache licensed, you're more than welcome to reuse any of its code in your own applications.

Reason #1: Testing

Testing is something that's rarely given enough credibility in software development projects. Notice that I didn't say that it doesn't get credit in software development publications! Many articles and case studies illustrate instances when test-first development and high test coverage can increase the quality of software. Regardless, testing is often viewed as something that simply lengthens a project's time to completion. In reality, if you use test-first methodologies to write your tests before your code, I believe you'll find that you'll actually speed up your development time. Furthermore, testing first makes maintenance and refactoring much easier. If you don't write code to test your code, you'll need to manually test your application by clicking through it -- and that's simply not very productive. Automation is key.

When you first start using AppFuse, you'll probably read the QuickStart Guide and Tutorials available on the project Web site (see Resources for links). The tutorials are written so that you write your tests first; they won't compile until you write the interface and/or implementation. If you're anything like me, you really should write the tests before you start writing code; it's the only way you'll be truly motivated to write them. If you write the implementation first and somehow verify that it works, you'll probably say to yourself, "Heck, it seems to be OK -- who needs a test? I've got more code to write!" The unfortunate aspect of this scenario is that you most likely did something to test that code; you simply skipped the part where you automated that test.

AppFuse's documentation shows you how to test all of the layers of your application. It starts at the database layer and uses DbUnit (see Resources) to pre-populate your database with test data before running tests. At the data access (DAO) layer, it uses Spring's AbstractTransactionalDataSourceSpringContextTests class (yes, that's a real class name!) to allow easy loading of Spring context files. Furthermore, this class wraps a transaction around each testXXX() method and rolls it back when the test method exits. This feature makes testing your DAO logic easy and does not affect the data in your database.

In the service layer, jMock (see Resources) is used to write true unit tests that mock their DAO dependencies. This allows for quick and fast tests that verify that your business logic is correct; you don't need to worry about about the underlying persistence logic.

HtmlUnit support
The HtmlUnit team has done a fair amount of work in the 1.8 release to make sure the package works with popular Ajax frameworks like Prototype and Scriptaculous.

In the Web tier, tests verify that actions (Struts/WebWork), controllers (Spring MVC), pages (Tapestry), and managed beans (JSF) work as you'd expect them to. Spring's spring-mock.jar is useful for testing all of these frameworks because it contains a mock implementation of the Servlet API. Testing AppFuse's Web frameworks would be difficult without this useful library.

The UI is usually the most difficult part to develop in a Web application. It's the piece that customers most often complain about -- either because it's not pretty or because it doesn't work the way they expect it to work. Plus, nothing is worse than doing a demo in front of a customer and seeing a stacktrace in the middle of it! Your application may be awesome, but your customer will have questions about your thoroughness. Don't let it happen. Canoo WebTest tests the UI. It employs HtmlUnit under the covers to walk through your UI, verify that elements are present and fill in form fields, and even validate that a fancy Ajax-enabled UI is working as expected. (See Resources for links to both WebTest and HtmlUnit.)

To make Web testing even easier, Cargo (see Resources) automates the starting and stopping of Tomcat before and after WebTest tests are run.



Back to top


Reason #2: Integration

As I mentioned in this article's introduction, many open source libraries are pre-integrated in AppFuse. They fall into the following categories:

  • Building, reporting, and code generation: Ant, Ant Contrib Tasks, Checkstyle, EMMA, Java2Html, PMD, and Rename Packages
  • Testing frameworks: DbUnit, Dumbster, jMock, JUnit, and Canoo WebTest
  • Database drivers: MySQL and PostgreSQL
  • Persistence frameworks: Hibernate and iBATIS
  • IoC frameworks: Spring
  • Web frameworks: JSF, Spring MVC, Struts, Tapestry, and WebWork
  • Web services: XFire
  • Web utilities: Clickstream, Display Tag, DWR, JSTL, SiteMesh, Struts Menu, and URL Rewrite Filter
  • Security: Acegi Security
  • JavaScript and CSS: Scriptaculous, Prototype, and Mike Stenhouse's CSS Framework

In addition to these libraries, AppFuse uses Log4j for logging and Velocity to construct e-mail and menu templates. Tomcat is supported out of the box for development, and you can use versions 1.4 or 5 of the Java platform to compile and build. You should be able to deploy AppFuse on any J2EE 1.3-compliant application server; it has been tested and is known to work on all the major J2EE servers and all of the major servlet containers.

Figure 3 shows the lib directory from the previously created devworks project. The lib.properties file in this directory controls the version numbers for each dependency, which means that you can easily test a new version of any of these packages by dropping it in this directory and issuing a command line like ant test-all -Dspring.version=2.0.


Figure 3. Project dependencies
AppFuse project dependencies

Having all of these open source libraries pre-integrated can supply a huge boost in productivity at the beginning of a project. Although you can find a fair amount of documentation for integrating these libraries, customizing a working example and simply using it to develop your application is much easier.

Besides simplifying the development of Web applications, AppFuse allows you to integrate Web services into your projects easily. XFire is included with the AppFuse download, but you can also integrate Apache Axis if you'd prefer (see Resources for the Axis integration tutorial). Together, the Spring framework and XFire make exposing your service layer as Web services very easy, giving you the ability to develop a service-oriented architecture.

In addition, AppFuse does not lock you into any particular API. It's simply repackaging and pre-integrating the best open source solutions available. The code that exists in AppFuse handles this integration and implements AppFuse's basic security and usability features. When possible, code is removed in favor of adding a feature to one of AppFuse's dependent frameworks. For example, AppFuse's home-grown Remember Me and SSL switching features were recently removed in favor of similar features from Acegi Security.



Back to top


Reason #3: Automation

Ant makes automation easy, from compiling to building to deploying. Ant is a first-class citizen in AppFuse, primarily because I've found it easier to run things from the command line than from an IDE. You can build, test, deploy, and perform any code-generation tasks using Ant.

Although this capability is great for some people, it doesn't work for everyone. Many AppFuse users currently use Eclipse or Intellij IDEA to build and test their projects. Running Ant from inside these IDEs works, but it isn't nearly as productive as running tests using the IDEs' built-in JUnit support.

Luckily, AppFuse supports running tests from the IDEs themselves, but maintaining this feature has been difficult for AppFuse developers. The biggest pain point is the fact that XDoclet is used to generate Hibernate mapping files and some artifacts for the Web frameworks (such as ActionForms and struts-config.xml for Struts). IDEs are unaware that code needs to be generated unless you configure them to build with Ant or install some sort of XDoclet-aware plug-in.

This lack of awareness is one of the primary reasons that AppFuse 2.0 will move to JDK 5 and Maven 2. JDK 5, annotations, and Struts 2 will allow us to get rid of XDoclet. Maven 2 will generate IDE project files, and with these generated files and dynamic classpaths, managing your project will be easier. The current Ant-based build system already produces artifacts for the different layers (including dao.jar, service.jar, and webapp.war), so moving to Maven's model should be a natural fit.

In addition to Ant (and a rich set of targets for building, testing, deploying, and reporting), support for CruiseControl is built into AppFuse. CruiseControl is a Continuous Integration application that allows you to automatically run all of your tests whenever code changes in your source code repository. The extras/cruisecontrol directory contains the files you need to quickly and easily set up Continuous Integration for your AppFuse-based project.

Setting up Continuous Integration is one of the first things you should do in a software development cycle. It not only motivates programmers to write tests, it also promotes teamwork and bonding through "You broke the build!" games.



Back to top


Reason #4: Security features and extensibility

AppFuse was originally developed as part of a sample application for a book I wrote for Apress, Pro JSP. This sample application demonstrated many security features and features for simplifying Struts development. Many of the security features in this application did not exist in J2EE's security paradigm. Authentication using container-managed authentication (CMA) was easy, but Remember Me, password hints, SSL switching, signup, and user management were nonexistent. Furthermore, the ability to protect methods based on roles was not possible in a non-EJB environment.

At first, AppFuse implemented all of these features with its own code and workarounds for CMA. I'd heard about Acegi Security when I first started learning Spring in early 2004. I compared the number of lines of XML required by Acegi (175) with the number that CMA required in web.xml (20) and quickly dismissed Acegi as too complicated.

A year and a half later -- and after writing a chapter about using Acegi Security for another book, Spring Live -- I had changed my mind. Acegi did (and still does) require a fair amount of XML, but it really is quite simple once you understand it. When we finally took the plunge and replaced all AppFuse's home-grown features with Acegi Security's features, we ended up deleting a lot of code. Classes upon classes went away, disappearing into the "Acegi handles that now" pile in CVS's Attic.

Acegi Security is simply the best thing that's ever happened to J2EE's security model. It allows you to implement many useful features that aren't part of the Servlet API's security model: authentication, authorization, role-protected methods, Remember Me, password encryption, SSL switching, user switching, and logout. It also allows you to store your user's credentials in an XML file, in a database, in LDAP, or in a single sign-on system such as Yale's Central Authentication Service (CAS) or SiteMinder.

AppFuse's implementation of many security-related features was nice in the beginning. Now that AppFuse uses Acegi Security, these features -- and many more -- are easy to implement. Acegi has many points for extension: that is the reason for its large XML configuration file. As we've integrated Acegi over the course of the last year, we've found that we've customized many bean definitions to hook into AppFuse more closely.

The combined ease of development, easily testable code, and loose coupling provided by the Spring IoC container and Acegi Security are the primary reasons that AppFuse is such a pleasure to develop with. These frameworks are nonintrusive and allow clean, testable code. AppFuse integrates many open source projects, and dependency injection allows easy integration of your application's layers.



Back to top


Reason #5: Code generation with AppGen

Some people call code generation code smell. In their opinion, if you need to generate code, you must be doing something wrong. I would argue that the ability to identify the patterns your code uses and automate their generation is code perfume. If you're writing similar DAOs, managers, and actions or controllers and you're not generating the code for them, that's a code smell. Sure, it's nice when the language gives you features like generics to make things easier, but often code generation is a necessary -- and extremely productive -- task.

AppFuse ships with an Ant- and XDoclet-based code-generation tool named AppGen. By default, generic DAOs and managers allow you to CRUD any plain old Java object (POJO), but doing that at the Web tier is difficult. AppGen has several features for performing the following tasks:

  • Generating POJOs from database tables (using the Middlegen and Hibernate tools)
  • Generating the UI from POJOs
  • Generating tests for DAOs, managers, actions/controllers, and the UI

When you run AppGen, you're prompted to generate from a database table or a POJO, and AppGen generates the code. If you issue ant install-detailed at the command line, AppGen installs POJO-specific DAOs and managers with their tests. Running ant install causes your Web tier classes to reuse the generic DAO and manager that exist by default.

To illustrate how AppGen works, I've created the table illustrated in Listing 2 in the devworks MySQL database.:


Listing 2. Create a database table named cat
    create table cat (
      cat_id int(8) auto_increment,
      color varchar(20) not null,
      name varchar(20) not null,
      created_date datetime not null,
      primary key (cat_id)
    ) type=InnoDB;

From the extras/appgen directory, run ant install-detailed. The full output from this command is too long to list for this article, but I've supplied the first part in Listing 3:


Listing 3. Running AppGen's install-detailed target
$ ant install-detailed
Buildfile: build.xml

init:
    [mkdir] Created dir: /Users/mraible/Work/devworks/extras/appgen/build
     [echo] 
     [echo] +-------------------------------------------------------+
     [echo] |             -- Welcome to the AppGen! --              |
     [echo] |                                                       |
     [echo] | Use the "install" target to use the generic DAO and   |
     [echo] | Manager, or use "install-detailed" to general a DAO   |
     [echo] | and Manager specifically for your model object.       |
     [echo] +-------------------------------------------------------+

    [input] Would you like to generate code from a table or POJO? (table,pojo)
table
    [input] What is the name of your table (i.e. person)?
cat
    [input] What is the name, if any, of the module for your table (i.e. organization)?

     [echo] Running Middlegen to generate POJO...

To use the newly generated code for the cat table, you need to modify src/dao/com/ibm/dao/hibernate/applicationContext-hibernate.xml to add the Cat.hbm.xml mapping file for Hibernate. Listing 4 illustrates what your modified sessionFactory bean should look like:


Listing 4. Adding Cat.hbm.xml to your sessionFactory bean
    <bean id="sessionFactory" class="...">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>com/ibm/model/Role.hbm.xml</value>
            <value>com/ibm/model/User.hbm.xml</value>
            <value>com/ibm/model/Cat.hbm.xml</value>
        </list>
    </property>
    ...
</bean>

After running ant setup deploy, you should be able to CRUD the cat table from the deployed application:


Figure 4. Cat list
Generated master screen

Figure 5. Cat form
Generated detail screen

The records you see in the above screenshots are created as part of the code-generation process, so there's data to test against.



Back to top


Reason #6: Documentation

You can find tutorials for each of the flavors of AppFuse, and you can find them in six different languages: Chinese, German, English, Korean, Portuguese, and Spanish. By flavors, I mean the different framework combinations, such as Spring MVC plus iBATIS, Spring MVC plus Hibernate, or JSF plus Hibernate. With five Web frameworks and two persistence frameworks, several combinations are possible. Related to these translations is the fact that AppFuse ships with eight translations for its default features. The available languages include Chinese, Dutch, German, English, French, Italian, Portuguese, and Spanish.

In addition to the core tutorials, many supplemental tutorials (see Resources) have been added to show integration with various databases, application servers, and other open source technologies (including JasperReports, Lucene, Eclipse, Drools, Axis.and DWR).



Back to top


Reason #7: Community

The Apache Software Foundation has an interesting perspective on open source. It's most interested in developing a community around its open source projects. Its members believe that if a community is strong, high-quality code will be a natural progression. To quote the text on the Apache home page:

"We consider ourselves not simply a group of projects sharing a server, but rather a community of developers and users."

The AppFuse community has grown tremendously since it started as a SourceForge project (part of struts.sf.net) in 2003. With the move to java.net in March of 2004, it became a popular project there and was the most accessed project from January through March of 2005. Today it remains a popular project (see Resources for links to java.net's project statistics) but is losing some ground to many of the Sun-sponsored projects on the site.

In late 2004, Nathan Anderson was brought on board as the first committer besides me. Many others have been added since, including Ben Gill, David Carter, Mika Göckel, Sanjiv Jivan, and Thomas Gaudin. All of the existing committers have contributed one way or another, and all have helped to make the AppFuse community a vibrant and fun place to be.

The mailing list is friendly, and we try to maintain the mantra that "no question is a dumb question." The only "RTFM" in our mailing list archives was from a user, not a developer. We definitely believe in the Apache open source philosophy. To quote my good friend Bruce Snyder, "We come for the code and stay for the people." Currently, most developers are users, and we generally like to have a good time. In addition, most of the documentation has been written by the community; therefore, the community is very knowledgeable.



Back to top


Conclusion

You should try developing with AppFuse because it allows you to easily test, integrate, automate, secure, and generate your Web applications. Its documentation is plentiful and its community is friendly. As the frameworks that power AppFuse get better, it will only continue to improve.

With AppFuse 2.0, we plan to move to JDK 5 (while still supporting deploying to 1.4) and Maven 2. These tools will simplify developing, installing, and upgrading with AppFuse. We plan to leverage Maven 2's ability to handle transitive dependencies. You'll encounter artifacts such as appfuse-hibernate-2.0.jar and appfuse-jsf-2.0.jar. These artifacts will be referenced in your pom.xml file and will be responsible for grabbing the rest of the associated dependencies. Rather than having AppFuse's base classes in your project, you'll simply extend the classes in the JARs, as with a normal framework, which should make it a lot easier to upgrade and encourage more users to submit their desired tweaks to the project.

If nothing else, using AppFuse will keep you at the forefront of Java Web development -- like we are!



Back to top


Resources

Learn

Get products and technologies
  • AppFuse on java.net: Download different flavors of AppFuse.

  • WebWork: Check out this easy-to-use Web framework.

  • DbUnit: Find out more about this JUnit extension.

  • jMock: Create dynamic mock objects to simplify true unit testing.

  • Canoo WebTest: Automate the UI testing of your Web applications.

  • HtmlUnit: The muscle behind WebTest's excellent JavaScript support.

  • Cargo: Automate starting and stopping your container.

  • Greenbox: A code-generation framework.


Discuss


Back to top


About the author

Matt Raible lives in Denver, Colorado, where he is a Spring and Web frameworks practice leader for Virtuas Open Source Solutions. He has extensive experience and expertise in open source, as both a user and a developer. Matt is the author of Spring Live from SourceBeat Publishing. He has also contributed to the Apress book Pro JSP Third Edition. He is a frequent presenter at open source conferences, including ApacheCon, MySQL User's Conference, and OSCON, and he is an active blogger on http://raibledesigns.com. Raible has been surrounded by computers for most of his life, even though he grew up in the backwoods of Montana without electricity. When he's not working, he's trying to make his wife, Julie, the happiest woman in the world or playing with their children, Abbie and Jack.




Back to top


 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值