Tuesday, February 17, 2009
Monday, February 9, 2009
Maven2 - BlazeDS, ActiveMQ and Flex (part2)
Assuming you followed along for "Maven2 - BlazeDS, ActiveMQ and Flex project structure"...
Go into your parent project and create the web module.
That just created a new module under the parent called "myproject-web". Now edit the pom.xml in that newly created project and add in the dependencies. I'm using Spring and Hibernate at the moment.
Also fix the compiler so that it compiles to 1.6 (1.5 if you prefer).
Ok, it should build at this point. Add your spring context (exercise to the reader) to the src/main/webapp/WEB-INF folder.
Edit the web.xml file so that the webapp knows to load your context.
Next up, a configuration module. Since we're using BlazeDS, there are a couple files that need to be compiled into the SWF as well as present in the webapp. Having them in two places and somehow remembering to keep them in sync is a pretty clear DRY violation. To get around that, we're going to put them in a module all on their own and then add that to the appropriate modules.
We don't need the src/test or src/main/java directories, so delete them and create a src/main/resources directory.
Inside the src/main/resources directory, create the services-config.xml file we'll need. This sets up the channels we'll need for our AMF communication.
Next, add the messaging-config.xml file.
Next up, we need to edit the myproject-config/pom.xml so that these resources get bundled up into a ZIP file (so we can more easily distribute them). Make a myproject-config/src/main/assembly directory then add a file called resources.xml and copy the following into it:
And change the myproject-config/pom.xml so that those resources get built. Get rid of the dependencies element since we don't need it. Add a build execution as follows.
Config module is done (use mvn install to test it, you should get a zip file containing the 2 resources) and now we need to get it into the webapp and Flex modules.
Edit the myproject-web/pom.xml file and add the dependency:
And the code to unpack the resource zip:
Also into the plugins section you should put the flex-compiler-mojo:
Since our Flex RIA is also using the config module, edit the myproject-ria/pom.xml and add the dependency just like above:
The plugin configuration is similar to the webapp one, but it has to change just slightly so that the config resources get extracted to a directory to get slurped up by the Flex mojo plugin:
Almost there, now we need to make sure the Flex swf file gets bundled into the web-app, so edit the myproject-web/pom.xml and add it as a dependency (watch out for the <type>swf</type>)
And the code to unpack it (also in myproject-web/pom.xml):
Go into your parent project and create the web module.
mvn archetype:create -DgroupId=mycompany -DartifactId=myproject-web -DarchetypeArtifactId=maven-archetype-webappThat just created a new module under the parent called "myproject-web". Now edit the pom.xml in that newly created project and add in the dependencies. I'm using Spring and Hibernate at the moment.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>3.2.6.ga</version>
</dependency>
Also fix the compiler so that it compiles to 1.6 (1.5 if you prefer).
<build>
...
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
Ok, it should build at this point. Add your spring context (exercise to the reader) to the src/main/webapp/WEB-INF folder.
Edit the web.xml file so that the webapp knows to load your context.
Next up, a configuration module. Since we're using BlazeDS, there are a couple files that need to be compiled into the SWF as well as present in the webapp. Having them in two places and somehow remembering to keep them in sync is a pretty clear DRY violation. To get around that, we're going to put them in a module all on their own and then add that to the appropriate modules.
mvn archetype:create -DgroupId=mycompany -DartifactId=myapplication-configWe don't need the src/test or src/main/java directories, so delete them and create a src/main/resources directory.
Inside the src/main/resources directory, create the services-config.xml file we'll need. This sets up the channels we'll need for our AMF communication.
<?xml version="1.0" encoding="UTF-8"?>
<services-config>
<services>
<service-include file-path="messaging-config.xml" />
</services>
<security />
<channels>
<channel-definition id="my-amf" class="mx.messaging.channels.AMFChannel">
<endpoint uri="http://{server.name}:{server.port}/{context.root}/messagebroker/amf" class="flex.messaging.endpoints.AMFEndpoint"/>
<properties>
<polling-enabled>false</polling-enabled>
</properties>
</channel-definition>
<channel-definition id="my-streaming-amf" class="mx.messaging.channels.StreamingAMFChannel">
<endpoint url="/messagebroker/streamingamf" class="flex.messaging.endpoints.StreamingAMFEndpoint"/>
<properties>
<idle-timeout-minutes>0</idle-timeout-minutes>
</properties>
</channel-definition>
<channel-definition id="my-secure-amf" class="mx.messaging.channels.SecureAMFChannel">
<endpoint uri="https://{server.name}:9100/{context.root}/messagebroker/amfsecure" class="flex.messaging.endpoints.SecureAMFEndpoint"/>
</channel-definition>
<channel-definition id="my-polling-amf" class="mx.messaging.channels.AMFChannel">
<endpoint uri="http://{server.name}:{server.port}/{context.root}/messagebroker/amfpolling" class="flex.messaging.endpoints.AMFEndpoint"/>
<properties>
<polling-enabled>true</polling-enabled>
<polling-interval-seconds>8</polling-interval-seconds>
</properties>
</channel-definition>
<channel-definition id="my-rtmp" class="mx.messaging.channels.RTMPChannel">
<endpoint uri="rtmp://{server.name}:2039" class="flex.messaging.endpoints.RTMPEndpoint"/>
<properties>
<idle-timeout-minutes>20</idle-timeout-minutes>
<client-to-server-maxbps>100K</client-to-server-maxbps>
<server-to-client-maxbps>100K</server-to-client-maxbps>
</properties>
</channel-definition>
<channel-definition id="my-http" class="mx.messaging.channels.HTTPChannel">
<endpoint uri="http://{server.name}:{server.port}/{context.root}/messagebroker/http" class="flex.messaging.endpoints.HTTPEndpoint"/>
</channel-definition>
<channel-definition id="my-secure-http" class="mx.messaging.channels.SecureHTTPChannel">
<endpoint uri="https://{server.name}:9100/{context.root}/messagebroker/httpsecure" class="flex.messaging.endpoints.SecureHTTPEndpoint"/>
</channel-definition>
</channels>
<logging>
<target class="flex.messaging.log.ConsoleTarget" level="Debug">
<properties>
<prefix>[Flex] </prefix>
<includeDate>true</includeDate>
<includeTime>true</includeTime>
<includeLevel>true</includeLevel>
<includeCategory>true</includeCategory>
</properties>
<filters>
<pattern>Endpoint.*</pattern>
<pattern>Service.*</pattern>
<pattern>Configuration</pattern>
</filters>
</target>
</logging>
<system>
<redeploy>
<enabled>true</enabled>
<watch-interval>20</watch-interval>
<watch-file>{context.root}/WEB-INF/flex/services-config.xml</watch-file>
<watch-file>{context.root}/WEB-INF/flex/messaging-config.xml</watch-file>
<touch-file>{context.root}/WEB-INF/web.xml</touch-file>
</redeploy>
</system>
</services-config>
Next, add the messaging-config.xml file.
<?xml version="1.0" encoding="UTF-8"?>
<service id="message-service" class="flex.messaging.services.MessageService">
<adapters>
<adapter-definition id="actionscript"
class="flex.messaging.services.messaging.adapters.ActionScriptAdapter"
default="true" />
<adapter-definition id="jms"
class="flex.messaging.services.messaging.adapters.JMSAdapter" />
</adapters>
<destination id="message-destination">
<properties>
<jms>
<destination-type>Topic</destination-type>
<message-type>javax.jms.ObjectMessage
</message-type>
<connection-factory>
java:comp/env/jms/flex/ActiveMqConnectionFactory
</connection-factory>
<destination-jndi-name>java:comp/env/jms/myTopic
</destination-jndi-name>
<delivery-mode>NON_PERSISTENT</delivery-mode>
<message-priority>DEFAULT_PRIORITY
</message-priority>
<acknowledge-mode>AUTO_ACKNOWLEDGE
</acknowledge-mode>
<initial-context-environment>
<property>
<name>Context.INITIAL_CONTEXT_FACTORY
</name>
<value>org.apache.activemq.jndi.ActiveMQInitialContextFactory
</value>
</property>
<property>
<name>Context.PROVIDER_URL</name>
<value>tcp://mycompanyurl:61616
</value>
</property>
</initial-context-environment>
</jms>
</properties>
<channels>
<channel ref="my-streaming-amf" />
</channels>
<adapter ref="jms" />
</destination>
</service>
Next up, we need to edit the myproject-config/pom.xml so that these resources get bundled up into a ZIP file (so we can more easily distribute them). Make a myproject-config/src/main/assembly directory then add a file called resources.xml and copy the following into it:
<assembly>
<id>resources</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>src/main/resources</directory>
<outputDirectory></outputDirectory>
</fileSet>
</fileSets>
</assembly>
And change the myproject-config/pom.xml so that those resources get built. Get rid of the dependencies element since we don't need it. Add a build execution as follows.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>make shared resources</id>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptors>
<descriptor>src/main/assembly/resources.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Config module is done (use mvn install to test it, you should get a zip file containing the 2 resources) and now we need to get it into the webapp and Flex modules.
Edit the myproject-web/pom.xml file and add the dependency:
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>myproject-config</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>resources</classifier>
<type>zip</type>
<scope>provided</scope>
</dependency>
And the code to unpack the resource zip:
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack-config</id>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/flex</outputDirectory>
<includeArtifacIds>todolist-config</includeArtifacIds>
<includeGroupIds>${project.groupId}</includeGroupIds>
<includeClassifiers>resources</includeClassifiers>
<excludeTransitive>true</excludeTransitive>
<excludeTypes>jar,swf</excludeTypes>
</configuration>
</execution>
</executions>
</plugin>
Also into the plugins section you should put the flex-compiler-mojo:
<plugin>
<groupId>info.rvin.mojo</groupId>
<artifactId>flex-compiler-mojo</artifactId>
<extensions>true</extensions>
</plugin>
Since our Flex RIA is also using the config module, edit the myproject-ria/pom.xml and add the dependency just like above:
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>myproject-config</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>resources</classifier>
<type>zip</type>
<scope>provided</scope>
</dependency>
The plugin configuration is similar to the webapp one, but it has to change just slightly so that the config resources get extracted to a directory to get slurped up by the Flex mojo plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack-config</id>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<phase>generate-resources</phase>
<configuration><outputDirectory>${project.build.directory}/generated-resources</outputDirectory>
<includeArtifacIds>myproject-config</includeArtifacIds>
<includeGroupIds>${project.groupId}</includeGroupIds>
<excludeTransitive>true</excludeTransitive>
</configuration>
</execution>
</executions>
</plugin>
Almost there, now we need to make sure the Flex swf file gets bundled into the web-app, so edit the myproject-web/pom.xml and add it as a dependency (watch out for the <type>swf</type>)
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>myproject-ria</artifactId>
<version>1.0-SNAPSHOT</version>
<type>swf</type>
</dependency>
And the code to unpack it (also in myproject-web/pom.xml):
<execution>
<id>copy-swf</id>
<phase>process-classes</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/${project.build.finalName}</outputDirectory>
<includeTypes>swf</includeTypes>
</configuration>
</execution>
Maven2 - BlazeDS, ActiveMQ and Flex project structure
(this is mostly the same as the great article from Sebastien Arbogast, I'm adapting it slightly to our needs and will focus more on the Eclipse/debug integration later on, but you should really read his post)
First up, getting the Maven project laid out!
I don't know of any way to generate the parent pom directly, so it's a couple quick steps to clean this up.
2) Go into the "myproject" directory and delete the src folder
3) Edit the pom.xml file and change the packaging element to "pom" instead of "jar" (while you're at it, go ahead and change the JUnit version to 4.5, might as well stay current)
Now that we've got the parent pom set up, let's start adding subprojects. We're going to need a Flex module, you have your choice of a couple projects, but the most promising one seems to be flex-mojos
First a quick stop back at our parent pom.xml file, add in the following before the dependencies element:
NOW create the Flex project... (this is all on ONE line)
If you open up the pom.xml in the parent folder, we should see a new "modules" section
You should also now see another folder in this directory called "myproject-ria". We've got to make a couple quick changes here too... first remove the "properties" element since we're not going to use the "israfil" plugin. Next remove the plugin section itself and replace it with
As the last step in the myproject-ria pom.xml, add the Flex dependencies...
Whew, that was rough! But now you should be able to go to the parent folder and run
First up, getting the Maven project laid out!
mvn archetype:create -DgroupId=com.mycompany -DartifactId=myprojectI don't know of any way to generate the parent pom directly, so it's a couple quick steps to clean this up.
2) Go into the "myproject" directory and delete the src folder
3) Edit the pom.xml file and change the packaging element to "pom" instead of "jar" (while you're at it, go ahead and change the JUnit version to 4.5, might as well stay current)
Now that we've got the parent pom set up, let's start adding subprojects. We're going to need a Flex module, you have your choice of a couple projects, but the most promising one seems to be flex-mojos
First a quick stop back at our parent pom.xml file, add in the following before the dependencies element:
<repositories>Then after the dependencies section add in:
<repository>
<id>flex-mojos-repository</id>
<url>http://flex-mojos.googlecode.com/svn/trunk/repository/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>flex-mojos-repository</id>
<url>http://flex-mojos.googlecode.com/svn/trunk/repository/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-3</version>
</plugin>
</plugins>
</pluginManagement>
</build>
NOW create the Flex project... (this is all on ONE line)
mvn archetype:create -DarchetypeArtifactId=maven-archetype-flex -DarchetypeVersion=1.0 -DarchetypeGroupId=dk.jacobve.maven.archetypes -DgroupId=mycompany -DartifactId=myproject-ria -DpackageName=com.donbestIf you open up the pom.xml in the parent folder, we should see a new "modules" section
<modules>
<module>mycompany-ria</module>
</modules>
You should also now see another folder in this directory called "myproject-ria". We've got to make a couple quick changes here too... first remove the "properties" element since we're not going to use the "israfil" plugin. Next remove the plugin section itself and replace it with
<plugin>
<groupId>info.rvin.mojo</groupId>
<artifactId>flex-compiler-mojo</artifactId>
<version>1.0-beta7</version>
<extensions>true</extensions>
<configuration>
<locales>
<param>en_US</param>
</locales>
</configuration>
</plugin>
As the last step in the myproject-ria pom.xml, add the Flex dependencies...
<dependencies>
<dependency>
<groupId>com.adobe.flex.sdk</groupId>
<artifactId>playerglobal</artifactId>
<version>3.0.0.3.0.0.477</version>
<type>swc</type>
<scope>external</scope>
</dependency>
<dependency>
<groupId>com.adobe.flex.sdk</groupId>
<artifactId>flex</artifactId>
<version>3.0.0.3.0.0.477</version>
<type>swc</type>
</dependency>
<dependency>
<groupId>com.adobe.flex.sdk</groupId>
<artifactId>framework</artifactId>
<version>3.0.0.3.0.0.477</version>
<type>swc</type>
</dependency>
<dependency>
<groupId>com.adobe.flex.sdk</groupId>
<artifactId>framework</artifactId>
<version>3.0.0.3.0.0.477</version>
<type>resource-bundle</type>
<classifier>en_US</classifier>
</dependency>
<dependency>
<groupId>com.adobe.flex.sdk</groupId>
<artifactId>rpc</artifactId>
<version>3.0.0.3.0.0.477</version>
<type>swc</type>
</dependency>
<dependency>
<groupId>com.adobe.flex.sdk</groupId>
<artifactId>rpc</artifactId>
<version>3.0.0.3.0.0.477</version>
<type>resource-bundle</type>
<classifier>en_US</classifier>
</dependency>
<dependency>
<groupId>com.adobe.flex.sdk</groupId>
<artifactId>utilities</artifactId>
<version>3.0.0.3.0.0.477</version>
<type>swc</type>
</dependency>
</dependencies>
Whew, that was rough! But now you should be able to go to the parent folder and run
mvn install
Thursday, February 5, 2009
Jetty and double-slashes in the URL
Hit a really brief snag today with Jetty and Flex...
Basically, in order to make your Flex app portable for different environments, you want to use tokens in the server url
e.g. this snippet from a Flex services-config.xml
See the {context.root} part?
If you have this project generate foo.war and deploy it to a server, you would access it through a URL like http://myhost/foo/
So,
The Flex client will connect to: http://myhost/foo/messagebroker/amf
Now suppose you want to deploy as the root webapp instead... URL becomes http://myhost/
So,
The Flex client tries connecting to http://myhost//messagebroker/amf (note the double slash because the context root is actually blank).
This actually works fine in Tomcat and IIS, but Jetty takes a different interpretation of the double slash and throws up a 404.
Luckily there's an easy fix if you're using Maven and the jetty-maven-plugin like I am... just edit the 'pom.xml' and add
Basically, in order to make your Flex app portable for different environments, you want to use tokens in the server url
e.g. this snippet from a Flex services-config.xml
<channels>
<channel-definition id="my-amf" class="mx.messaging.channels.AMFChannel">
<endpoint url="http://{server.name}:{server.port}/{context.root}/messagebroker/amf" class="flex.messaging.endpoints.AMFEndpoint">
</channel-definition>
...
</channels>
See the {context.root} part?
If you have this project generate foo.war and deploy it to a server, you would access it through a URL like http://myhost/foo/
So,
server.name=foo
server.port=80
context.root=foo
The Flex client will connect to: http://myhost/foo/messagebroker/amf
Now suppose you want to deploy as the root webapp instead... URL becomes http://myhost/
So,
server.name=foo
server.port=80
context.root=<empty>
The Flex client tries connecting to http://myhost//messagebroker/amf (note the double slash because the context root is actually blank).
This actually works fine in Tomcat and IIS, but Jetty takes a different interpretation of the double slash and throws up a 404.
HTTP ERROR: 404
NOT_FOUND
RequestURI=//messagebroker/amf
Powered by Jetty://
Luckily there's an easy fix if you're using Maven and the jetty-maven-plugin like I am... just edit the 'pom.xml' and add
<compactPath>true</compactPath>
<plugins>
...
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>80</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
<webAppConfig>
<contextPath>/</contextPath>
<compactPath>true</compactPath>
</webAppConfig>
</configuration>
</plugin>
...
</plugins>
Sunday, February 1, 2009
long time no blog
Been a few weeks since last post, but I haven't been sitting idle. Got a very simple proof-of-concept going with real-time scores using JMS messaging to a BlazeDS server streaming to an Adobe Flex client (this is all overlaying a legacy system). Pretty exciting stuff and remarkably simple. It's all bleeding edge at this point - going to have to write a something like the guys over at Farata Systems did (NIO connector so we can support enough customers on BlazeDS).
Friday, January 9, 2009
Getting Started with Git on Ubuntu
I'm going to be taking a look at Git and how well/if it will fit in with an existing Subversion repository, so first thing to do is get it installed (I'm using Ubuntu 8.04 so your mileage may vary, but I followed the exact same steps on my laptop with 8.10)
First off - DON'T use the version in the Ubuntu repositories, it's out of date - but this is still super easy.
Grab the latest source tarball from the Git site. (was 1.6.1 at the time of writing this)
Open up a terminal.
Go into the directory where you saved the git tarball.
(Note: I'm installing it for all users, you could leave the 'prefix=/usr' part out to only install it for yourself. Also, this step will take a while...)
Assuming all went well, you should see something like:
First off - DON'T use the version in the Ubuntu repositories, it's out of date - but this is still super easy.
Grab the latest source tarball from the Git site. (was 1.6.1 at the time of writing this)
Open up a terminal.
$ sudo apt-get build-dep git-core git-docGo into the directory where you saved the git tarball.
$ tar -zxvf git-1.6.1.tar.gz
$ cd git-1.6.1/
$ sudo make prefix=/usr all doc(Note: I'm installing it for all users, you could leave the 'prefix=/usr' part out to only install it for yourself. Also, this step will take a while...)
$ sudo make prefix=/usr install install-doc
$ git --versionAssuming all went well, you should see something like:
git version 1.6.1
Tuesday, January 6, 2009
Apple doesn't want iPhone developers?
Spent a little time over the holiday break thinking about potentially profitable applications for the iPhone... Finally came up with an idea I'm a little excited about. Ok, so it's probably not profitable, but at least it would be fun to write. Unfortunately, I hit a roadblock. I registered for the Apple Developer Connection to get the SDK to get started and couldn't find one for Linux.
A quick email to Apple returned:
"Thank you for contacting the Apple Developer Connection regarding the iPhone Developer Program.
The iPhone SDK requires an Intel processor-based Mac running Mac OS X Leopard. Please know that Apple does not offer a version of the iPhone SDK for Windows operating systems and we have not announced any plans to do so."
Have to admit I'm a little miffed about that - I'd love to have a nice Mac but if I don't have one by now, I'm sure not buying one just to develop an iPhone app. This seems a bit shortsighted on Apple's part. Oh well, Android seems pretty interesting too... hopefully there are some new Android-based phone offerings soon. In the meantime, here's a link to a recently published title from my favorite series:
Hello, Android
[edit: after thinking it over some more, it's pretty smart, albeit distasteful. The amount of sales that they're generating on the iPhone creates a large target for developers, who of course have no choice but to buy a Mac to develop an app for that market - a double win for Apple]
A quick email to Apple returned:
"Thank you for contacting the Apple Developer Connection regarding the iPhone Developer Program.
The iPhone SDK requires an Intel processor-based Mac running Mac OS X Leopard. Please know that Apple does not offer a version of the iPhone SDK for Windows operating systems and we have not announced any plans to do so."
Have to admit I'm a little miffed about that - I'd love to have a nice Mac but if I don't have one by now, I'm sure not buying one just to develop an iPhone app. This seems a bit shortsighted on Apple's part. Oh well, Android seems pretty interesting too... hopefully there are some new Android-based phone offerings soon. In the meantime, here's a link to a recently published title from my favorite series:
Hello, Android
[edit: after thinking it over some more, it's pretty smart, albeit distasteful. The amount of sales that they're generating on the iPhone creates a large target for developers, who of course have no choice but to buy a Mac to develop an app for that market - a double win for Apple]
Subscribe to:
Posts (Atom)