Questions to Consider When Starting an eCommerce Site
by Frank Kim on Jun.12, 2009, under Consulting
I have done many eCommerce projects and am looking forward to doing more.
The most interesting eCommerce projects are the ones that are starting from scratch. I was fortunate to be a part of several such projects including NFLShop.com, CasualMale.com and OriginalPenguin.com. Of these the OriginalPenguin.com project was by far the most interesting because I was simultaneously the director, lead engineer, and QA.
Whenever I start a project or am consulting for one I ask the following questions to help determine the scope and range of the project.
- What technology will be used? J2EE? ATG? .Net? PHP? Ruby on Rails?
- What platform? Windows? Linux? Solaris? FreeBSD?
- How will it be hosted? Locally? Shared host? Virtual host? Exclusive host?
- What database do you use or plan to use?
- How many products, product categories and SKU’s do you have?
- How will catalog administration be done? Should it be part of the web application? Or will you use a separate third-party application?
- How will you manage price lists?
- How will you keep track of inventory?
- When an order is submitted, how will it be fulfilled? Who does fulfillment?
- How will you handle payments? Payflow Pro? Cyber Cash? CyberSource? PayPal?
- How will you handle taxes? TAXWARE?
- What kind of security do you want? Will everything be handled securely via SSL? Do you already have an SSL server certificate for the site?
- Will you require that to buy something you have to have an account? If not will you want to still try to encourage buyers to get an account? Will you be saving credit card numbers with the account?
- Can buyers track their order history, order status, etc.?
- What kind of emails do you want sent during the order/fulfillment process?
- Will you want to implement promotions and/or coupons?
- What kind of catalog navigation do you want? Do you want a menu like navigation like Amazon.com?
- Do you want the buyer to be able to search for items?
- Do you want product comparison?
Unit Test for Threaded Logging
by Frank Kim on May.04, 2009, under Java SE, Logging
Brian Ploetz sent me this great unit test for threaded logging. In it we are trying to find if a deadlock occurs.
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import junit.framework.TestCase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Appender;
import org.apache.log4j.Logger;
/**
* Unit test for the ThrottlingFilter in a multi-threaded environment
*/
public class ThrottlingFilterThreadUTest extends TestCase {
private static final Log logger = LogFactory.getLog(ThrottlingFilterThreadUTest.class);
private static ThreadMXBean threadMXBean;
@Override
protected void setUp() throws Exception {
super.setUp();
threadMXBean = ManagementFactory.getThreadMXBean();
logger.info("Thread contention monitoring supported: "
+ threadMXBean.isThreadContentionMonitoringSupported());
logger.info("Thread contention monitoring enabled: "
+ threadMXBean.isThreadContentionMonitoringEnabled());
threadMXBean.setThreadContentionMonitoringEnabled(true);
logger.info("Thread contention monitoring enabled: "
+ threadMXBean.isThreadContentionMonitoringEnabled());
}
/**
* Tests multiple threads using the same filter instance at the same time
*/
public void testThreads() {
Logger rootLogger = Logger.getRootLogger();
assertNotNull(rootLogger);
Appender fileAppender = rootLogger.getAppender("FILE");
assertNotNull(fileAppender);
ThrottlingFilter throttlingFilter = (ThrottlingFilter) fileAppender.getFilter();
assertNotNull(throttlingFilter);
ThreadGroup infoThreadGroup = new ThreadGroup("info-group");
ThreadGroup errorThreadGroup = new ThreadGroup("error-group");
Thread errorThread1 = new ErrorThread(errorThreadGroup, "error-thread-1");
Thread infoThread1 = new InfoThread(infoThreadGroup, "info-thread-1");
Thread errorThread2 = new ErrorThread(errorThreadGroup, "error-thread-2");
Thread infoThread2 = new InfoThread(infoThreadGroup, "info-thread-2");
infoThread1.start();
errorThread1.start();
errorThread2.start();
infoThread2.start();
while (true) {
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds());
for (int i = 0; i < threadInfos.length; i++) {
ThreadInfo threadInfo = threadInfos[i];
if (threadInfo != null && threadInfo.getThreadState() == Thread.State.BLOCKED) {
System.out.println("Thread '" + threadInfo.getThreadName()
+ "' is blocked on the monitor lock '" + threadInfo.getLockName()
+ "' held by thread '" + threadInfo.getLockOwnerName() + "'");
}
}
if (!infoThread1.isAlive() && !errorThread1.isAlive() && !infoThread2.isAlive()
&& !errorThread2.isAlive())
break;
}
}
public static class ErrorThread extends Thread {
private static final Log logger = LogFactory.getLog(ErrorThread.class);
public ErrorThread(ThreadGroup tg, String name) {
super(tg, name);
}
public void run() {
for (int i = 0; i < 10; i++) {
try {
test(0);
} catch (Exception e) {
long start = System.currentTimeMillis();
logger.error("Error!", e);
long end = System.currentTimeMillis();
System.out.println("Took " + (end-start) + "ms to log error");
}
}
}
// simulate large stack traces
private void test(int i) {
if (i >= 500)
throw new RuntimeException("D'OH!");
test(i+1);
}
}
public static class InfoThread extends Thread {
private static final Log logger = LogFactory.getLog(InfoThread.class);
public InfoThread(ThreadGroup tg, String name) {
super(tg, name);
}
public void run() {
for (int i = 0; i < 100; i++) {
logger.info("Hi!");
}
}
}
}
The log4j.xml test file.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"
debug="false">
<!-- ================================= -->
<!-- Appenders -->
<!-- ================================= -->
<!-- A time/date based rolling file appender -->
<appender name="FILE"
class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="server.log" />
<param name="Append" value="true" />
<!-- Rollover at midnight each day -->
<param name="DatePattern" value="'.'yyyy-MM-dd" />
<layout class="org.apache.log4j.PatternLayout">
<!-- The default pattern: Date Priority [Category] Message\n -->
<param name="ConversionPattern" value="%d %-5p [%c] %m%n" />
</layout>
<filter class="com.betweengo.log4j.ThrottlingFilter">
<param name="maxCountSameMessage" value="100"/>
<param name="maxCountSavedMessages" value="100"/>
<param name="waitInterval" value="60"/>
</filter>
</appender>
<!-- ======================= -->
<!-- Setup the Root category -->
<!-- ======================= -->
<root>
<level value="INFO" />
<appender-ref ref="FILE" />
</root>
</log4j:configuration>
Get JSTL Vars from PageContext
by Frank Kim on May.04, 2009, under JSTL
JSTL sets its vars in the pageContext. For example:
pageContext.setAttribute("foo", bar);
Therefore to get a JSTL variable use the pageContext within a tag or a JSP page. For example:
// get item from pageContext and put in request
atg.servlet.DynamoHttpServletRequest drequest = atg.servlet.ServletUtil.getDynamoRequest(request);
drequest.setParameter("foo", pageContext.getAttribute("foo"));
YouTube Embedded Player Parameters
by Frank Kim on Apr.17, 2009, under Web
Here is a page describing the YouTube Embedded Player Parameters. These parameters are query parameters you can add to the end of the YouTube URL.
There are different parameters you can add to the object and embed statements. Here is an example.
<object width="320" height="265"> <param name="movie" value="http://www.youtube.com/v/6sHenGpeGgo&hl=en&fs=1&autoplay=1"></param> <param name="allowFullScreen" value="true"></param> <param name="allowscriptaccess" value="always"></param <param name="wmode" value="transparent"></param> <embed src="http://www.youtube.com/v/6sHenGpeGgo&hl=en&fs=1&autoplay=1" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" wmode="transparent" width="320" height="265"></embed> </object>
HTTP Proxy
by Frank Kim on Apr.16, 2009, under Web
Recently I was having a problem where a Flash video was not being displayed. It turned out the Flash video had not been created properly and had a dependency on other Flash files that were not available. When I was viewing this Flash video directly using JBoss I did not see any errors. But when I viewed it using Apache the logs showed the dependency problems.
Another developer, Allan Scott, suggested next time I use Charles or Fiddler as HTTP proxies.
Did you try watching the HTTP traffic with a tool like Fiddler or Charles?
If there is a dependency on something and it failed to download it then you should be able to see the 404 or some other error.
Getting the request parameter
by Frank Kim on Apr.10, 2009, under JSTL, Page Development
I always forget how to do this so I thought I should write it down.
In JSP:
<%=request.getParameter("foo")%>
<img src="<%=request.getParameter("foo")%>">
In JSTL:
<c:out value="${param.foo}"/>
In DSP:
<dspel:valueof param="foo"/>
Encode URI
by Frank Kim on Mar.17, 2009, under Java SE
To encode an URI you can simply use Java’s URLEncoder’s encode method which has been available since JDK 1.4.
String encodedUri;
try {
encodedUri = URLEncoder.encode(uri, "UTF-8");
}
catch (UnsupportedEncodingException exc) {
// this should never happen
logger.warn("UTF-8 is not a supported encoding? Not encoding for now...", exc);
encodedUri = uri;
}
Unobtrusive JavaScript
by Frank Kim on Mar.17, 2009, under JavaScript
Unobtrusive JavaScript’s goal is to move all the functionality (the Controller) out of the HTML (the Model) and the CSS (the View). This is also called full MVC separation.
This is how typically JavaScript and HTML mix. For example in submitting a form using a text link.
<a href="javascript:submit(document.testForm)">Submit</a>
Or for jumping to different parts of a page using a dropdown.
<input type="image" src="/img/buttons/update.gif" onClick="window.location.hash= document.jumpTo.names.options[document.jumpTo.names.selectedIndex].value">
Using the unobtrusive JavaScript technique one can separate the JavaScript from the HTML.
<div id="foo">Submit</div>
<script type="text/javascript">
Event.observe($('foo'), 'click', function(event) {
$(Event.element(event)).form.submit();
return false;
});
</script>
We use the Protoculous JavaScript file which combines the Prototype framework and Scriptaculous libraries to do unobtrusive JavaScript.
mod_rewrite to bypass security
by Frank Kim on Mar.02, 2009, under HTTP Server, Struts
Many Apache webserver installations use uriworkermap to configure requests are forwarded to Tomcat/JBoss and which are not. This provides a certain level of security. For example:
## APACHE RESOURCES (static files): !/*.gif=myapp !/*.html=myapp ## DISALLOW (security-related filter): !/*.jsp=myapp !/*.xml=myapp ## TOMCAT RESOURCES: /*.do=myapp
However if you dynamically generate your sitemap.xml or any other XML files using a servlet then this security will be a problem since the XML request will not make it to Tomcat/JBoss. This is when mod_rewrite comes to the rescue.
You can set up mod_rewrite to rewrite the sitemap.xml request to be a sitemap.do request.
RewriteRule ^/sitemap\.xml$ /sitemap.do [PT,L]
Then you can set up Struts to forward this request to sitemap.xml.
<action path="/sitemap" forward="/sitemap.xml"/>
Dynamically generate sitemap.xml
by Frank Kim on Mar.02, 2009, under Servlet
sitemap.xml is a top level document on your website “for webmasters to inform search engines about pages on their sites that are available for crawling.” Google not surprisingly has its own documentation on how to improve your site’s visibility using sitemap.xml.
Typically sitemap.xml is a static file that is hand generated. But on large sites it makes more sense to generate this dynamically. One way to do this is to generate it on demand using a servlet. Here is my simple solution. I did not include the implementation for outputPages() since that will be specific to each application server’s DB hierarchy or web server’s file structure.
public class SiteMap extends HttpServlet {
protected static final String MIME_TYPE_XML = "application/xml";
// XML tags
protected static final String SITE_MAP_XML_INFO = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
protected static final String SITE_MAP_BEGIN =
"<urlset\n\txmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9\n\t\thttp://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\">";
protected static final String SITE_MAP_END = "</urlset>";
protected static final String LOC_BEGIN = " <loc>";
protected static final String LOC_END = "</loc>";
protected static final String PRIORITY_BEGIN = " <priority>";
protected static final String PRIORITY_END = "</priority>";
protected static final String URL_BEGIN = "<url>";
protected static final String URL_END = "</url>";
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
// set content type to be XML
response.setContentType(MIME_TYPE_XML);
// get writer
PrintWriter out = response.getWriter();
// output header
out.println(SITE_MAP_XML_INFO);
out.println(SITE_MAP_BEGIN);
// output pages
outputPages(request, out);
// output end
out.println(SITE_MAP_END);
out.close();
}
protected void outputPage(String uri, String priority, PrintWriter out, String urlStart) {
out.println(URL_BEGIN);
out.println(LOC_BEGIN + urlStart + uri + LOC_END);
out.println(PRIORITY_BEGIN + priority + PRIORITY_END);
out.println(URL_END);
}
}
Then you configure web.xml to use the SiteMap servlet.
<servlet>
<servlet-name>sitemap</servlet-name>
<servlet-class>com.upromise.olm.app.servlet.SiteMap</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>sitemap</servlet-name>
<url-pattern>/sitemap.xml</url-pattern>
</servlet-mapping>
