The rating of my talk at the Jazoon just came in: 2.74 on a scale from 1 to 5. That's even below average (3 would be average). Hm. Okay, I was sick and tried to put too much information into my 40 minutes. Anything else I can do better next time?
Monday, July 30, 2007
Wednesday, July 25, 2007
Quickly disabling tests
Ever needed to disable all (or most) tests in a JUnit test case?
How about this: Using the editor of your choice, search for "void test" and replace all of them with "void dtest" ("d" as in disabled). Now, you can simply enable the few tests you need to run by deleting the "d" again.
I'm also using "x" to take out tests that won't run for a while. Using global search in the whole project, it's also simple to find them again just in case you're wondering if there are any disabled tests left.
Monday, July 23, 2007
Testing BIRT
I'm a huge fan of TDD. Recently, I had to write tests for BIRT, specifically for a bug we've stumbled upon in BIRT 2.1 that has been fixed in 2.2: Page breaks in tables.
The first step was to setup BIRT so I can run it from my tests.
public IReportEngine getEngine () throws BirtException { EngineConfig config = new EngineConfig(); config.setLogConfig("/tmp/birt-log", Level.FINEST); // Path to the directory which contains "platform" config.setEngineHome(".../src/main/webapp"); PlatformConfig pc = new PlatformConfig (); pc.setBIRTHome(basepath); PlatformFileContext context = new PlatformFileContext(pc); config.setPlatformContext(context); Platform.startup(config); IReportEngineFactory factory = (IReportEngineFactory) Platform .createFactoryObject(IReportEngineFactory .EXTENSION_REPORT_ENGINE_FACTORY); if (factory == null) throw new RuntimeException ("Couldn't create factory"); return factory.createReportEngine(config); }
My main problems here: Find all the parts necessary to install BIRT, copy them to the right places and find out how to setup EngineConfig (especially the platform part).
public void renderPDF (OutputStream out, File reportDir, String reportFile, Map reportParam) throws EngineException { File f = new File (reportDir, reportFile); final IReportRunnable design = birtReportEngine .openReportDesign(f.getAbsolutePath()); //create task to run and render report final IRunAndRenderTask task = birtReportEngine .createRunAndRenderTask(design); // Set parameters for report task.setParameterValues(reportParam); //set output options final HTMLRenderOption options = new HTMLRenderOption(); options.setOutputFormat(HTMLRenderOption.OUTPUT_FORMAT_PDF); options.setOutputStream(out); task.setRenderOption(options); //run report task.run(); task.close(); }
I'm using HTMLRenderOption here so I could use the same code to generate HTML and PDF.
In my test case, I just write the output to a file:
public void testPageBreak () throws Exception { Map params = new HashMap (20); ... File dir = new File ("tmp"); if (!dir.exists()) dir.mkdirs(); File f = new File (dir, "pagebreak.pdf"); if (f.exists()) { if (!f.delete()) fail ("Can't delete "+f.getAbsolutePath() + "\nMaybe it's locked by AcrobatReader?"); } FileOutputStream out = new FileOutputStream (f); ReportGenerator gen = new ReportGenerator(); File basePath = new File ("../webapp/src/main/webapp/reports"); gen.generateToStream(out, basePath, "sewingAtelier.rptdesign" , params); if (!f.exists()) fail ("File wasn't written. Please check the BIRT logfile!"); }
Now, this is no test. It's only a test when it can verify that the output is correct. To do this, I use PDFBox:
PDDocument doc = PDDocument.load(new File ("tmp", "pagebreak.pdf")); // Check number of pages assertEquals (6, doc.getPageCount()); assertEquals ("Error on page 1", "...\n" + "...\n" + ... "..." , getText (doc, 1));
The meat is in getText():
private String getText (PDDocument doc, int page) throws IOException { PDFTextStripper textStripper = new PDFTextStripper (); textStripper.setStartPage(page); textStripper.setEndPage(page); String s = textStripper.getText(doc).trim(); Pattern DATE_TIME_PATTERN = Pattern.compile("^\\d\\d\\.\\d\\d\\.\\d\\d\\d\\d \\d\\d:\\d\\d Page (\\d+) of (\\d+)$", Pattern.MULTILINE); Matcher m = DATE_TIME_PATTERN.matcher(s); s = m.replaceAll("23.07.2007 14:02 Page $1 of $2"); return fixCRLF (s); }
I'm using several tricks here: I'm replacing a date/time string with a constant, I stabilize line ends (fixCRLF() contains String.replaceAll("\r\n", "\n");) and do this page by page to check the whole document.
Of course, since getText() just returns the text of a page as a String, you can use all the other operations to check that everything is where or as it should be.
Note that I'm using MockEJB and JNDI to hand a datasource to BIRT. The DB itself is Derby running in embedded mode. This allows me to connect to directly a Derby 10.2 database even though BIRT comes with Derby 10.1 (and saves me the hazzle to fix the classpath which OSGi builds for BIRT).
@Override protected void setUp () throws Exception { super.setUp(); MockContextFactory.setAsInitial(); Context ctx = new InitialContext(); MockContextFactory.setDelegateContext(ctx); EmbeddedDataSource ds = new EmbeddedDataSource (); ds.setDatabaseName("tmp/test_db/TestDB"); ds.setUser(""); ds.setPassword(""); ctx.bind("java:comp/env/jdbc/DB", ds); } @Override protected void tearDown () throws Exception { super.tearDown(); MockContextFactory.revertSetAsInitial(); }
Links:
What's Wrong With Java Part 2b
To give an idea why I needed 5KLoC for such a simple model, here is a detailed analysis of Keyword.java:
LoC | Used by |
---|---|
43 | Getters and setter |
40 | XML Import/Export |
27 | Model |
27 | equals()/hashCode() |
21 | Hibernate mapping with annotations |
14 | Imports |
2 | Logging |
174 | Total |
As you can see, boiler plate code like getter/setters and equals() need 70LoC or 40% (48% if you add imports). Mapping the model to XML is more expensive than mapping it to a database. In the next installment, we'll see that this can be reduced considerably.
Note: This is not a series of articles about flaws in Hibernate or the Java VM, this is about the Java language (ie. what you type into your IDE and then compile with javac).
Saturday, July 21, 2007
What's Wrong With Java Part 2
OR Mapping With Hibernate
After the model, let's look at the implementation. The first candidate is the most successful OR mapper combination in the Java world: Hibernate.
Hibernate brings all the features we need: It can lazy-load ordered and unordered data sets from the DB, map all kinds of weird relations and it lets us use Java for the model in a very comfortable way: We just plain Java (POJO's actually) and Hibernate does some magic behind the scenes that connects the objects to the database. What could be more simple?
Well, an OO language which is more dynamic, for example. Let's start with a simple task: Create a standalone keyword and put that into the DB. This is simple enough:
1: 2: 3: 4: 5: |
Keyword kw = new Keyword(); kw.setType (Keyword.KEYWORD); kw.setName ("test"); session.save (kw); |
(Please ignore the session object for now.)
That was easy, wasn't it? If you look at the log, you'll see that Hibernate sent an INSERT statement to the DB. Cool. So ... how do we use this new object? The first, most natural idea, would be to use the object we just saved:
1: 2: 3: 4: |
Knowledge k = new Knowledge (); k.addKeyword (kw); session.save (k); |
Unfortunately, this doesn't work. It does work in your test but in the final application, the Keyword is created in the first transaction and the Knowledge in the second one. So Hibernate will (rightfully) complain that you can't use that keyword anymore because someone else might have changed it.
Now, what? You have to ask Hibernate for a copy of every object after you closed the transaction in which you created it before you can use it anywhere else:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
Keyword kw = new Keyword(); kw.setType (Keyword.KEYWORD); kw.setName ("test"); session.save (kw); kw = dao.loadById (kw.getId ()); Knowledge k = new Knowledge (); k.addKeyword (kw); session.save (k); |
Why do we have to load an object after just saving it? Well ... because of Java. Java has very strict rules what you can do with (or to) an object instance after it has been created. One of them is that you can't replace methods. So what, you'd think. In our case, things aren't that simple. In our model, the name of a Knowledge instance is a Keyword. When you look at the code, you'll see the standard setter. But when you run it, you'll see that someone loads the item from the KEYWORD table. What is going on?
1: 2: 3: |
public void setName (Keyword name) { this.name = name; } |
Behind the scenes, Hibernate replaces this method by using a proxy object, so it can notice when you change the model (setting a new name). The most simple soltuion would be to replace the method setName() in session.save() with calls the original setter and notifies Hibernate about the modification. In Python, that's three lines of code. Unfortunately, this is impossible in Java.
So to get this proxy objects, you must show an object to Hibernate, let it make a copy (by calling save()) and then ask for the new copy which is in fact a wrapper object that behaves just like your original object but it also knows when to send commands to the database. Simple, eh?
Makes me wonder why session.save() doesn't simply return the new object when it is more safe to use it from now on ... especially when you have a model which is modified over several transactions. In this case, you can easily end up with a mix of native and proxy objects which will cause no end of headache.
Anyway. This approach has a few drawbacks:
- If someone else creates the object, calls your code and then continues to do something with the original object (because people usually don't expect methods to replace objects with copies when they call them), you're in deep trouble. Usually, you can't change that other code. You loose. Go away.
- The proxy object is very similar but not the same as the original object. The biggest difference is that it has a different class. This means, in equals(), you can't use this.getClass == other.getClass(). Instead, you have to use instanceof (the copy is derived from the original class). This breaks the contract of equals() which says that it must be symmetric.
- If you have large, complex objects, copying them is expensive.
- After a while, you will start to write factory methods that create the objects for you. The code is always the same: Create a simple object, save it, load it again and then return the copy. Apart from cut&paste, this means that you must not call new for some of your objects. Again, this breaks habits which leads to bugs.
All in all, the whole approach is clumsy. Really, it's not Hibernate's fault but the code is still ugly, hard to maintain (because it breaks the implicit rules we have become so used to). In Python, you just create the object and use it. The dynamic nature of Python allows the OR mapper to replace or wrap all the methods as it needs to and you never notice it. The code is clean, easy to understand and compact.
Another problem are the XML config files. Besides all the issues with Java XML parsers, it is always problematic to store the same information in two places. If you ever change your Java model, you better not forget to update the XML or you will get strange errors. You can't refactor the model classes anymore because there is code outside the scope of your refactoring tool. And let's not forget code completion which works pretty good for Java. Not so for XML files. If you're lucky, someone has written a code completion for your type of XML config. Still, there will be problems. If there is a new version, your code completion will lag behind.
It's like regexp: Some people, when confronted with a problem, think "I know, I'll use regular expressions." Now they have two problems. -- Jamie Zawinski
Fortunately, Sun solved this problem with JPA (or at least eased the pain). JPA allows to use annotations to store the mapping configuration in the class file itself. Apart from a few small problems (like setting up everything), this works pretty well. Code completion works perfectly because any IDE which has code completion will be able to use the latest and greatest version of your helper JARs without any customization. Just drop the new JAR in your classpath and you're ready to do. Swell.
But there are more problems:
- You must create a session object "somewhere" and hand it around. If you're writing a webapp, this better be thread-safe. Not to mention you must be able to override this for tests.
- The session object must track if you have already started a transaction and nest them properly or you will have to duplicate code because you can't call existing methods if they use transactions.
- Spring and AOP will help a lot but they also add another layer of complexity, you'll have to learn another API, another set of rules how to organize your code, etc.
- JAR file-size. My code is 246KB. The JARs it depends on take ... 6'096KB, more than 40 times of my code. And I'm not even using Spring.
- Even with JPA, Hibernate is not simple to use because Java itself is not simple to use.
In the end, the model was 5'400 LoC. A added a small UI to it using SWT/JFace which added 2'400 LoC.
If you look at the model in the previous installment, then the question is: Why do I need 5'000 LoC to write a program which implements an OR mapper for a model which has only three classes and 26 lines of code?
Granted, test cases and helper code take their toll. I could accept that this code needs four or five times the size of the model itself. Still, we have a gap.
The answer is that there are no or bad defaults. For our simple case, Hibernate could guess everything. Java could generate all the setters and getters, equals() and hashCode(). It's no black magic to figure out that Relation has a reference to Knowledge so there needs to be a database table which stores this information. Sadly, defaults in Java are always "safe" rather than "clever". This is the main difference to newer languages. They try to guess most of the stuff and then, you can fix those few exceptions that you always have. With Java, all the exceptions are handled but you have to do everyday stuff yourself.
The whole experience was frustrating, especially since I'm a seasoned Java developer. It took me almost two weeks to write the code for this small model mostly because because of a bug in Hibernate 3.1 and because I couldn't get my mind around the existing documentation. Also, parent-child relations were poorly documented in the first Hibernate book. The second book explains this much better.
Conclusion: Use it if you must. Today, there are better ways.
Next stop: TurboGears, a Python web framework using SQL Objects.
Wednesday, July 11, 2007
What's Wrong With Java, Part 1
As I promised at the Jazoon, I'm starting a series of posts here to present the reasons behind my thoughts on the future on Java. To do this, I'll develop a small knowledge management application with the name Sensei, the Japanese word for "teacher". The final goal is to have three versions in Java, Python an Groovy at the same (or at least a similar) level.
The application will sport the usual features: A database layer, a model and a user interface. Let's start with the model which is fairly simple.
We have knowledge nodes that contain the knowledge (a short name and a longer description), keywords to mark knowledge nodes and relations to connect knowledge nodes. Additionally, we want to be able to organize knowledge nodes in a tree. Here is the UML:
To make it easier to search the model for names, I collect them in the keyword class. So a knowledge node has no String field with the name but a keyword field instead. The same applies to the relations. Here is what the Java model looks like:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: |
class Keyword { public enum Type { KEYWORD, KNOWLEDGE, RELATION }; static Set<Keyword> allKeywords; Type type; String name; } class Knowledge { Keyword name; String knowledge; Knowledge parent; List<Knowledge> children; Set<Keyword> keywords; } class Relation { Keyword name; Knowledge from; Knowledge to; } |
Note that I omitted all the usual cruft (private fields, getters/setters, equals/hashCode).
This model has some interesting properties:
- There is a recursive element. Many OR mappers show lots of example how to map a String to a field but for some reason, examples with tree-like structures are scarce.
- It contains 1:N mappings where N can be 0 (keywords can be attached to knowledge nodes but don't have to be), 1:N mappings where N is always >= 1 (names of knowledge nodes and relations) and N:M mappings (relations between knowledge nodes).
- It contains a field called "from" which is an SQL keyword.
- There are sorted and unsorted mappings (children and keywords of a knowledge node).
- Instances of Keyword must be garbage collected somehow. When I delete the last knowledge node or relation with a certain name, I want the keyword deleted with it. This is not true for pure keywords, though.
Let's have a look at the features we want to support:
- Searching knowledge nodes by name or keyword or relation
- Searches should by case insensitive and allowing to search for substrings
- Adding child nodes to an existing knowledge node
- Reordering children
- Moving a child from one parent to another
- Adding and removing relations between nodes
- Adding and removing keywords to a knowledge node
In the next installment, we will have a look how Java and Hibernate can cope with these demands.
Monday, July 09, 2007
Building HTML
There are two ways to generate HTML: The right way and the JSP way. Enough has been said about JSP, what's the right way?
The right way is to have a programming language that is flexible enough to merge HTML and code painlessly. Groovy does it this way:
def builder = new MarkupBuilder (); builder.html { head { title getTitle() } body { genBody (builder) } }
What is going on here?
First of all, you must know that you can omit several things in Groovy: parentheses or semi-colon, for example. If we add these, the example becomes less readable but better understandable for Java developers:
def builder = new MarkupBuilder (); builder.html () { head () { title (getTitle()); }; body () { genBody (builder); }; };
So obviously, in the first line, a method html() is called. Now, you need to know that the code in curly brackets is a closure and that closure is added as the last parameter to the call of the method in front of it. This means the code in the curly brackets is passed into html() and can be executed any time html() wants to do it. It can even invoke the closure several times. In Java, the definition of html would be: html(Closure c).
The same happens with head(), title() (which takes a String argument) and body(). Now where are these methods defined?
Nowhere. MarkupBuilder() is a class which defines a "catch all" method called invokeMethod. Whenever Groovy cannot find the right method to invoke, it will check if a class defines
Object invokeMethod(String methodName, Object args)and invoke this method instead. The method gets the name of the original method and all the original parameters in a list.
In our case, MarkupBuilder.invokeMethod() will use the method name as the HTML element name. That's it. A little bit of flexibility in the parser got us a long way towards HTML without making the code unreadable.
Next, we need a flexible way to pass HTML attributes. In Groovy, named arguments are supported:
void genBody (MarkupBuilder builder) { builder.p (style:'font-weight: bold;', 'Impressive.') }
This will call MarkupBuilder.invokeMethod() with "p" as the method name and a list consisting of the map with the style and a string. This information will be used to build the element and it's content.
Of course, the builder will stream the output (which is very simple since it can wrap your code when it needs to: essentially it will just warp all the virtual methods you call), there is no way to forget a closing element (your program won't compile anymore) and splitting complex HTML into several methods is a cinch.
Life can be so simple with a little bit of flexibility.
Further reading: Using MarkupBuilder for Agile XML creation
Friday, July 06, 2007
Debugging AJAX Applications with IE
Note: Most of the information for this blog entry was copied from this blog (German only).
Debugging AJAX Applications (or RIA) has become much more simple with the Firefox extensions Web Developer Toolbar and the fantastic tool Firebug.
IE has lagged behind but there are now two tools which help a lot: IE Developer Toolbar which mimics the HTML/CSS editing capabilities of Firebug and some of the tools of WDT (like clearing the cache, showing outlines and disabling images). For debugging JavaScript, you can get the MS Script Debugger but you have to dig through the config to enable and disable it.
Now all I need is a way to get at the IE Dev Toolbar when the browser window has no menu ...
Thursday, July 05, 2007
What's Wrong With ... Surveillance
"If you have nothing to hide, you have nothing to fear from ubiquitous surveillance." Uhm, really?
Well, I have something to hide. It's nothing illegal. I just want to hide from a lot of people: Sales and marketing people, for example, who want to get my mon*cough*attention. People, who hate the company I work for (for whatever good or bad reason). People, who dislike my religion, my taste in clothes, politics or sex.
Imagine a male working for the London police. He's been dumped by his girlfriend, he's jealous or just seeking revenge. He sits in his little office and tracks her moving around the city with the some of the 500'000 cameras in the city. Eventually, he sees her meeting with her new flame. What will he do?
Maybe he will not use the face recognition software (which was pretty useless a few years ago). But there are other way. The new boyfriend of "his" girl will probably walk to his car (identification by license plate is a standard tool for the police and you wouldn't believe the zoom levels the surveillance cameras can get if you don't limit them artificially) or he will go home. Guess who is having a surprise visit tonight? In 2003, the LA Times brought an article "LA Police Officer Uses Database to Snoop on the Stars". Apparently, this fellow was looking for a way to even out his income by selling juicy details to tabloids.
The problem with surveillance is not that I have nothing to hide, it's that I don't trust all the people who operate the system. In order to "increase" the safety of the system, little is known about which directly leads to a sense of untouchability by the people who run them. We have seen where this leads. Power doesn't corrupt, unaccountability does.
But there are other problems as well. In Germany, a camera was installed to protect a museum but it also watched the private flat of Angela Merkel (German only). Don't worry, it watched her only for eight years.
This could be fixed by operating the cameras automatically by a computer. A judge could grant access to the files when authorities receive a complaint. Unfortunately, this just shifts the problem. For most people, computers are still magical boxes. They know that it's just a bunch of cleverly arranged silicon atoms but the real problem is that they can't tell when a computer lies. Of course, that never happens. Right?
Well, computers don't lie in the sense that they can know fact A and tell you B. That's a human skill. But a human can delete fact A and replace it with fact B and the computer will happily present fact B as The Truth(TM). Since security systems are by default accessible by a select few only, it becomes increasingly hard to know if someone has tampered with a system. Worse, someone can accidentally break something. Your name might suddenly appear on the persona non grata list of the USA because someone mistyped the last name of an evil doer who has the same birthday as you (a chance of 1:366 or less). Luckily, you will notice the next time you pass through customs. Enjoy your strip-search if they don't arrest or shoot you on sight.
"But the computer said ..." Several billion will find this funny, one person won't. Of course, this is an exaggerated example. But quite a few people do find themselves at the special attention of customs and they don't know why. That is because the victims aren't informed about the mistake (the culprit already knows, the guy who made the mistake is sure he didn't and the person who eventually finds out is too embarrassed to talk about it). Even when they eventually find out, it is insanely hard for to get the mistake fixed everywhere. So when you have finally made sure the guys at airport A know you're cool, the computer at airport B might not know or might not trust that new information. After all, you might be a very clever cracker, trying to clear your slate! Can't trust nobody!
Any system that is supposed to be secure, must allow for error, especially human error. When I was taught engineering, the rule was to make each piece twice as strong as it needed to be if a human life was in some way connected to it. That meant you could hang a small car to a swing and it wouldn't break (don't try; they have optimized the process since then). The security systems that are being sold to us today are sold as "infallible". Like the Titanic, the Hindenburg, Bank computers, "automatic" invoice systems. They can't make mistakes, so when one happens, no one will ask any questions. Somehow, everyone seems to forget that there are still very few computers that can read (and none who can understand what they just read; just ask Google ... and they get the data in a computer readable format). Most data that you can find in any computer on this planet has been planted there by humans! Especially the data about other humans! Or as Thomas R. Fasulo said in his infamous IH8PCs blog: "You should never believe anything you read or hear. Especially if you read it here. "
Furthermore, the wide spread surveillance is sold under the flag of "safety". We are supposed to be more safe. How so? The number of crimes doesn't change. A few more crimes can be resolved because of the surveillance but the idea that they prevent them is foolish. People commit crimes because they believe they won't be caught. If there is a camera, they will just adjust their strategy, not change their lives. Many of them believe that the reasons for their behavior is outside of their own control, so they really can't do anything. On the other hand, imagine the torture of a rape victim that is being filmed in the act and the criminal doesn't get caught.
Unfortunately, the surveillance systems are sold as a cheap solution for the underlying problems. If a kid has no perspective in life and only gang members as role models, what choice does it have? You would be astonished. Take the Bronx, turned into the sin pit of the world by the media. In 2000, there lived roughly 400'000 people between 10 and 25. In that year, a total of 48,070 crimes were recorded. If each was committed by a different individual, that means that 88% of the people followed the law (remember, even if they were not caught, the crime is still recorded). Sadly, spending millions of dollars for CCTV cameras is more cheap (as in simple) than trying to solve the real problems.
More safety by more surveillance? I don't buy it.
Monday, July 02, 2007
The Elevator Problem or Why Writing Software Is So Hard
A lot of people wonder why writing high quality software is so hard. When you read this, you're probably sitting in front of a computer that runs an operating system with millions lines of code. Most people know that a computer "computes", that is it adds numbers all the time and because we define some numbers to mean characters (65 usually means A) or colors (0 is black, 16777215 is pure white, 16711680 is bright green, etc), it can do astonishing things. Like showing you something a guy on the other side of the globe wrote. It's all just math.
As we all know, math isn't simple but 1+1 always gives 2. It doesn't sometimes give 1.9 or 2.2 or 3 or 1. It's 2. Always. That basic observation made many people believe that it must be simple to write correct software. It's just math, after all. It's exact. Well, let me tell you a story so you understand why writing software is so hard and sometimes insane.
Imagine you're working in a big company. Your company is so big, it has its own skyscraper. There are six elevators that carry all your co-employees in and out, every day. This morning, your boss storms into the room and he's obviously very upset: For the third day in a row, someone is using his parking lot. You say, no problem, you'll put a sign in the elevators that the parking lot #5 right next to the elevators is reserved. Your boss is very happy and you can see the next raise shining brightly at the horizon.
So you turn to your computer, fire up your favorite text tool and after a few minutes or hours (depending on the tool), you come up with a nice sign saying to stay the hell away from parking lot #5. A few minutes (or hours) later, your printer has delivered the signs on nice fresh paper. You grab 'em and walk to spread the news in all six elevators. It's the most simple thing in the world to press the button to call an elevator. You wait a few moments (or longer, it's a big building) and "bing", one of the six elevators stops by to get you somewhere.
You smile, step into the cabin and slab the first sign onto the wall. You brought Scotch tape, right? Of course you did. And scissors, too.
The sign looks great. Your boss will be very happy, he will think fondly of you when it's time to spread the good stuff.
So you leave the elevator, let's call it A, wait for the doors to close and press the call button once more.
"Bing" and the door of elevator A opens, so you can take the ride. You start to realize that this simple task might not be that simple after all.
There is a little computer somewhere in the building, that tries very hard to move the elevators around effectively and right now, it's very proud of itself because you didn't have to wait. Now, it eagerly waits for you to tell it where you want to go, so it can take you there as fast as possible. In the meantime, it does the same thing with the other five cabins.
After pondering the situation for a moment, you step inside the cabin and press the button of the floor that is farthest away from where you currently are. The door closes, you wait a moment, so it is really gone and call the next elevator.
"Bing", elevator A opens it's door again.
Apparently, it hasn't moved at all or it can move much faster than you'd think possible. Let us rule out the second option because it probably breaks some laws of physics. You do know that elevators measure the weight of the passengers, don't you? Well, some time ago, smart engineers noticed that kids wasted a lot of energy sending the empty cabins around. That made them angry, they didn't want little pests to toy with their toys. So they used the very same sensor to determine if someone is actually inside and if not, they told the little control computer to just ignore any requests to send the elevator around. Ha! That shows them!
All right, you tell yourself, and go for the staircase to move a few floors down. Slightly annoyed, you press the call button. You wait, "Bing" and ... elevator A offers it's service. Somewhere in the house, a little control computer is very proud that it could send you an empty ride so fast.
Now is the time to start to worry. You could ask your buddies in the office for help. You need five of them, each rides one elevator to some floor and blocks it there. Of course, you know them and you're a bit worried about the remarks you'll probably get before you can explain to them why on earth it could be so hard to stick a piece of paper on an elevator.
Now is also a good time to think what you boss will think when you don't return soon. Surely, to stick six pieces of paper on a wall can't take more then a few minutes? What will he think if you fail even such a simple task?
The morale of the story: Even very simple things can become insanely complex as soon as a computer is involved. Unfortunately, even seasoned developers with years of experience sometimes fail to see all the traps in advance. That leads to the paradox situation that they concentrate on all the "hard" stuff, which they expect to cause trouble and postpone the "easy" stuff. If that easy stuff is hiding a bad surprise, it's usually late in the project when you finally get to it. And at that time, the effects might be tremendous because you made your all your plans and estimates with the idea in mind that the "easy" thing will be a cinch.
That leads to the conclusion that it is impossible to estimate a software project except you have done the very same thing (including all circumstances!) before, preferably several times. If you didn't, your estimates are just guesswork and could be off by several magnitudes (i.e. 10, 100 or a 1000 times).
PS: The solution for the elevator problem is to find the janitor or someone from the cleaning team. They have keys to take cabins out of service (ever noticed the locks near the buttons? That's what they are for), so they can clean or repair them. Or you could paste the notice on the wall next to the parking lot. Or place one under the wiper. But we're computer freaks; simple solutions are for wimps!