Skip to Content »

Tech Life of Recht » archive for December, 2007

 2008 wishes

  • December 29th, 2007
  • 3:02 pm

The coming of a new year usually attracts a number of predictions, some more accurate than others. I’m no good at predicting, so I’ll give my wishes for the new year instead:

  • Get my Macbook Pro to work with the DVI-VGA adapter (done)
  • I’d like to be convinced that the dynamic languages are really the way forward. I see that they can do smart stuff, but I sorely miss some kind of help using whatever APIs are developed, besides the source code, and I can’t get used to working without a proper editor. I can probably live without the editor, but having methods like def initialize (options = {}) just hurts my eyes. I haven’t used Ruby/Rails that much, but I’ve already dug around more source code than I care to in order to find out what the legal options were.
  • Release Java 7 with proper closures. This probably won’t happen, and even when it does, it will take a couple of years before it’s adopted.
  • Already, I know that I’m going to spend a good part of my time fiddling around with huge WSDLs and schemas. Could somebody find a way to stop the black hole that is Web Services?
  • As for Linux on the desktop, that will still not happen, even though it should, especially when you look at the new distributions such as Ubuntu. However, I’m pretty satisfied, now I just need xmonad to support dynamic twinview, so I can add a monitor without restarting X
  • And to Apple: please take corporate customers seriously and make a docking station for the Macbook (Pro). I’m getting tired of plugging and unplugging 5 different cables every time I have to move my laptop.
  • Less focus on creating big monolithic systems, more on distributing across autonomous nodes.
  • Finally, it would be great with a proper vi-mode for Eclipse – like vimperator for Firefox.

 DVI adapter with Macbook Pro and Linux

  • December 29th, 2007
  • 2:36 am

I recently discovered that using the DVI adapter with the Macbook Pro wasn't as easy as I thought. The MBP comes with a DVI connector, which is great, and it works just fine when you plug a DVI monitor into it. However, when doing presentations, you almost always have to use a regular VGA connector. Even though the driver (nvidia) could see the screen, it refused to display anything.
Now I've finally worked out a solution, and luckily it's not that complicated. The trick is that even though the monitor is recognized, it is still treated as a DVI monitor. In order to get the driver to treat it as a normal VGA monitor, add this to xorg.conf:

CODE:
  1. Section "Device"
  2.   ...
  3.   Option "ConnectedMonitor" "DFP-0,DFP-1,CRT-0"
  4.   Option "MetaModes" "DFP-0: 1440x900, DFP-1: 1680x1050; DFP-0: 1440x900, CRT-0: 1280x1024"
  5. EndSection

Of course, the modes should be adjusted according to the setup. The MetaModes line should contain the valid combinations, separated by ;. In this case, I've specified that the internal display should always run at 1440x900, and then I either have a DVI monitor connected at 1680x1050 or a VGA monitor at 1280x1024. Of course, it's also possible just to specify one monitor if you're not using TwinView.

The trick now is that when running in X, xrandr can be used to switch between the monitors. When running xrandr, it should display something like this:

CODE:
  1. Screen 0: minimum 1280 x 900, current 1440 x 900, maximum 2720 x 1024
  2. default connected 1440x900+0+0 (normal left inverted right) 0mm x 0mm
  3.    1440x900    50.0*   51.0    54.0
  4.    2720x1024   52.0    50.0
  5.    2464x900    53.0
  6.    1280x1024   55.0

The tricky part now is that the nvidia driver "hides" the monitor configuration in the refresh rate. For example, the 1280x1024 resolution has only one rate, which means that this resolution is unique to one monitor. Switching to this mode with 'xrandr -s 1280x1024' will enable the VGA monitor and disable the internal display.
However, the mode 2720x1024 has two refresh rates, 52 and 50. Unfortunately there's no easy way of telling it, but the 52 rate is with the internal display and the VGA monitor on, while the 50 is with only the internal display on (and a larger desktop, something you probably don't want to use). Switching on the VGA monitor then requires two arguments: xrandr -s 2720x1024 --rate 52.

This isn't the easiest to work with, so I've created a small Perl script which can query the nvidia driver directly to find the available modes, and then call xrandr. Download the script here. To use it, the X11::Protocol Perl package must be installed, and of course xrandr and the nvidia driver. To use the script, simply call it without any arguments to get a list of the supported resolutions and monitors. Select a configuration by calling the script with -s where id is one of the numbers in the first column. That's about as simple as it gets.

 xmonad 0.5

  • December 16th, 2007
  • 12:57 am

xmonad 0.5 is out, and I haven't upgraded my installation in a while. It's pretty nice that configuration is now done in /.xmonad/xmonad.hs, then there's no need to recompile xmonad all the time. The configuration change did require a reorganisation of my old Config.hs, so I took a look at some of the samples to see what other people did.
I ended up with something which looks like this:

xmonad

This is my xmonad.hs:

CODE:
  1. -- XMonad Core
  2. import XMonad
  3. import XMonad.Layout
  4. import XMonad.Operations
  5. import qualified XMonad.StackSet as W
  6.  
  7. -- GHC hierarchical libraries
  8. import Data.Bits ((.|.))
  9. import qualified Data.Map as M
  10. import Graphics.X11
  11. import Graphics.X11.Xlib
  12. import Graphics.X11.Xlib.Extras
  13. import System.IO
  14.  
  15. -- Contribs
  16. import XMonad.Actions.CycleWS
  17. import XMonad.Actions.SwapWorkspaces
  18. import XMonad.Actions.Submap
  19. import XMonad.Actions.WindowBringer
  20. import XMonad.Actions.FloatKeys
  21.  
  22. import XMonad.Hooks.UrgencyHook
  23. import XMonad.Layout.NoBorders
  24. import XMonad.Layout.Tabbed
  25. import XMonad.Layout.WindowNavigation
  26. import XMonad.Layout.Grid
  27. import XMonad.Layout.LayoutHints
  28. import XMonad.Layout.Dishes
  29. import XMonad.Util.EZConfig
  30. import XMonad.Util.Run
  31.  
  32. import XMonad.Prompt.Shell
  33. import XMonad.Prompt
  34.  
  35. import XMonad.Hooks.DynamicLog   ( PP(..), dynamicLogWithPP, dzenColor, wrap, defaultPP )
  36.  
  37. myfont = "\"-xos4-terminus-medium-r-normal--12-120-72-72-c-60-iso8859-1\""
  38. fgcolor = "black"
  39. bgcolor = "white"
  40.  
  41. statusBarCmd= "dzen2 -e '' -w 660 -h 15 -ta l -xs 1 -fg " ++ fgcolor ++ " -bg " ++ bgcolor ++ " -fn " ++ myfont
  42.  
  43. -- Get ready!
  44. main = do din <- spawnPipe statusBarCmd
  45.           xmonad $ withUrgencyHook dzenUrgencyHook { args = ["-bg", "darkgreen", "-xs", "1"] }
  46.               $ defaultConfig
  47.                 { workspaces     = workspaces'
  48.                 , modMask        = modMask'
  49.                 , numlockMask    = 0
  50.                 , layoutHook     = layoutHook'
  51.                 , terminal       = "urxvtc || urxvt"
  52.     , normalBorderColor = "#dddddd"
  53.     , focusedBorderColor = "#3499dd"
  54.     , defaultGaps = [(15,0,0,0)]
  55.     , logHook = dynamicLogWithPP $ myPP din
  56.                 }
  57.                 `additionalKeys` keys'
  58.  
  59. modMask'    = mod4Mask
  60. workspaces' = map show [1] ++ ["web", "mail", "chat", "code"] ++ map show [6 .. 9 :: Int]
  61.  
  62. layoutHook' =
  63.     configurableNavigation noNavigateBorders $
  64.     layouts
  65. layouts =
  66.         Mirror tiled
  67.     ||| tiled
  68.     ||| Grid
  69.     ||| layoutHints Full
  70.     ||| Dishes 2 (1/5)
  71.     ||| noBorders (tabbed shrinkText
  72.                           defaultTConf { fontName = myfont })
  73.   where
  74.      tiled   = Tall nmaster delta ratio
  75.      nmaster = 2     -- The default number of windows in the master pane
  76.      ratio   = 1/2   -- Default proportion of screen occupied by master pane
  77.      delta   = 3/100 -- Percent of screen to increment by when resizing panes
  78. noFollow CrossingEvent {} = return False
  79. noFollow _                = return True
  80. keys' =
  81.     [ ((modMask' .|. shiftMask, xK_d        ), spawn "date | dzen2 -p 2 -xs 1") -- %! Print current date
  82.      , ((modMask', xK_p), shellPrompt defaultXPConfig)
  83.     ]
  84.     ++
  85.     -- modMask'-[1..0] %! Switch to workspace N
  86.     -- modMask'-shift-[1..0] %! Move client to workspace N
  87.     [((m .|. modMask', k), windows $ f i)
  88.         | (i, k) <- zip workspaces' $ [xK_1 .. xK_9] ++ [xK_0]
  89.         , (f, m) <- [(W.greedyView, 0), (W.shift, shiftMask)]]
  90.     ++
  91.     -- modMask'-{e,r} %! Switch to physical/Xinerama screens 1 or 2
  92.     -- modMask'-shift-{e,r} %! Move client to screen 1 or 2
  93.     [((m .|. modMask', key), screenWorkspace sc>>= flip whenJust (windows . f))
  94.         | (key, sc) <- zip [xK_e, xK_w] [0..]
  95.         , (f, m) <- [(W.view, 0), (W.shift, shiftMask)]]
  96.     ++
  97.     [((modMask' .|. mod1Mask, k), windows $ swapWithCurrent i)
  98.         | (i, k) <- zip workspaces' $ [xK_1 .. xK_9] ++ [xK_0]]
  99.     -- float keys
  100.     ++
  101.     [
  102.       ((modMask',               xK_d     ), withFocused (keysResizeWindow (-10,-10) (1,1)))
  103.     , ((modMask',               xK_s     ), withFocused (keysResizeWindow (10,10) (1,1)))
  104.     , ((modMask',               xK_a     ), withFocused (keysMoveWindowTo (512,384) (1, 0)))
  105.     ]
  106.  
  107.  
  108. myPP h = defaultPP
  109.          { ppCurrent  = dzenColor "white" "#cd8b00" . pad
  110.          , ppVisible  = dzenColor "white" "#666666" . pad
  111.          , ppHidden   = dzenColor "black" "#cccccc" . pad
  112.          , ppHiddenNoWindows = dzenColor "#999999" "#cccccc" . pad
  113.          , ppWsSep    = dzenColor "#bbbbbb" "#cccccc" "^r(1x18)"
  114.          , ppSep      = dzenColor "#bbbbbb" "#cccccc" "^r(1x18)"
  115.          , ppLayout   = dzenColor "black" "#cccccc" .
  116.                         (\ x -> case x of
  117.                                   "TilePrime Horizontal" ->
  118.                                     " ^i(/home/emertens/images/tile_horz.xpm) "
  119.                                   "TilePrime Vertical"   ->
  120.                                     " ^i(/home/emertens/images/tile_vert.xpm) "
  121.                                   "Hinted Full"          ->
  122.                                     " ^i(/home/emertens/images/fullscreen.xpm) "
  123.                                   _                      -> pad x
  124.                         )
  125.          , ppTitle    = (' ':) . escape
  126.          , ppOutput   = hPutStrLn h
  127.          }
  128.   where
  129.   escape = concatMap (\x -> if x == '^' then "^^" else [x])
  130.   pad = wrap " " " "

I use a simple shell script for starting xmonad:

CODE:
  1. ssh-add ~/.ssh/id_dsa </dev/null>/dev/null
  2. Esetroot /usr/share/backgrounds/warty-final-ubuntu.png
  3. unclutter -idle 2 &
  4. urxvtd -f -o
  5. irxevent -d
  6.  
  7. gnome-settings-daemon &
  8. gnome-volume-manager &
  9.  
  10. export PATH=$PATH:`dirname $0`
  11. `dirname $0`/status &
  12.  
  13. xmonad

Finally, the dzen2 status bar is run using this script:

CODE:
  1. #!/bin/sh
  2.  
  3. BG=white
  4. FG=black
  5. FONT="-xos4-terminus-medium-r-normal--12-120-72-72-c-60-iso8859-1"
  6.  
  7. BFG="#444"
  8.  
  9. gcpubar -i 2 -fg '#444' -w 30 -h 10 | dzen2 -e '' -x 1360 -fg $FG \
  10.    -bg $BG -fn $FONT -h 15 -xs 1 &
  11.  
  12. while :; do
  13. MEM=`memstatus.awk /proc/meminfo`
  14. CPU=`cpustatus.awk`
  15. DATE=`date +"%d-%m %k:%M"`
  16.  
  17. REM=`awk '/remaining capacity/ { print $3 }' /proc/acpi/battery/BAT0/state`
  18. LAST=`awk '/last full/ { print $4}' /proc/acpi/battery/BAT0/info`
  19. STATE=`awk '{print $2}' /proc/acpi/ac_adapter/ADP1/state`
  20. if [ "$STATE" = "on-line" ]; then
  21.   BAT=$(echo $REM $LAST | awk '{printf "Bat: %.1f%%, AC", ($1/$2)*100'})
  22. else
  23.   PRESENT=`awk '/present rate/ { print $3}' /proc/acpi/battery/BAT0/state`
  24.   BAT=$(echo $REM $LAST $PRESENT | \
  25.     awk '{printf "Bat: %.1f%%, %d min", ($1/$2)*100, ($1/$3)*60}')
  26. fi
  27.  
  28. LOAD=`awk '{print $1 " " $2 " " $3}' /proc/loadavg`
  29.  
  30. echo "$CPU $MEM | $DATE | $BAT | $LOAD"
  31. sleep 5
  32. done | dzen2 -e '' -x 660 -w 700 -fg $FG -bg $BG -fn $FONT -h 15 -xs 1

Two awk scripts are used - one for the memory usage and one for CPU frequencies. Why awk? No idea, it just seemed like a good choice at the time.

memstatus.awk:

CODE:
  1. #!/usr/bin/awk -f
  2. BEGIN {
  3.   BG="darkgrey";
  4.   FG="#444";
  5.   WIDTH=30;
  6.   HEIGHT=10;
  7. }
  8.  
  9. /MemTotal/ {t=$2};
  10. /MemFree/ {f=$2};
  11. /SwapTotal/ {st=$2};
  12. /SwapFree/ {sf=$2};
  13.  
  14. END {
  15.   mw=int(WIDTH*(t-f)/t);
  16.   fw=WIDTH-mw;
  17.   sw=int(WIDTH*(st-sf)/st);
  18.  
  19.   printf("Mem: ^ib(1)^fg(%s)^r(%dx%d)^fg(%s)^r(%dx%d)" \
  20.     "^fg(%s)^r(2x%d)^fg(%s)^r(%dx%d)^fg(%s)^r(%dx%d)^ib(0)^fg()\n",
  21.     FG, mw, HEIGHT,
  22.     BG, fw, HEIGHT,
  23.     "black",
  24.     HEIGHT, BG, WIDTH-sw,
  25.     HEIGHT, FG, sw, HEIGHT);
  26. }

CODE:
  1. #!/usr/bin/awk -f
  2. BEGIN {
  3.   BG="darkgrey"
  4.   FG="#444"
  5.   WIDTH=30
  6.   HEIGHT=10
  7.   CPUS=0
  8.  
  9.   cmd = "ls /sys/devices/system/cpu"
  10.   while ((cmd | getline)> 0) {
  11.     if ($1 ~ /cpu/) {
  12.       CPUS++
  13.     }
  14.   }
  15.   close(cmd)
  16.  
  17.   printf "CPU: "
  18.   for (i=0; i <CPUS; i++) {
  19.     cfn="/sys/devices/system/cpu/cpu" i "/cpufreq/scaling_cur_freq"
  20.     mfn="/sys/devices/system/cpu/cpu" i "/cpufreq/scaling_max_freq"
  21.     getline cf <cfn
  22.     getline mf <mfn
  23.     cw=int(WIDTH*(mf-cf)/mf)
  24.  
  25.     printf("^ib(1)^fg(%s)^r(%dx%d)^fg(%s)^r(%dx%d)^ib(0)^fg()",
  26.       FG, cw, HEIGHT, BG, WIDTH-cw, HEIGHT)
  27.  
  28.     close(cfn)
  29.     close(mfn)
  30.  
  31.     if (i <CPUS - 1) {
  32.       printf("^ib(0)^fg()^r(5x0)^ib(1)")
  33.     }
  34.  
  35.   }
  36.   printf("\n");
  37. }

 GWT and ruling the world

  • December 15th, 2007
  • 2:24 am

I like Google Web Toolkit. Well, I could probably even say love, but I don't want to go too far. However, one thing does irritate me: I usually build GWT projects using my Ant target, and I also use Ivy for dependency management. This means that I have no external dependencies, all jar files are downloaded automatically by Ivy and placed in lib/build. To make everything as easy as possible, the Ant classpath is just defined to be lib/build/*.jar, instead of singling out every one of the jar files.
This works pretty well, until you start using GWT, a recent version of Xerces, and want to do XML schema validation. Depending on the OS, different versions of Xerces are loaded, which results in spurious classpath errors - methods cannot be found, classes doesn't exist, operations not supported, and so on.
This goes on until you realize that GWT (in the form of gwt-dev-*.jar) includes all dependencies in a single jar file. This includes Ant, Xerces, other XML apis, and quite a lot of other stuff. I realize that it's easier to distribute, but why oh why can't we just get a regular jar file with the GWT classes in it? Anyways, in case anybody should be interested, the gwt-dev-*.jar file can be unpacked, anything but com.google.gwt deleted, and then these Ivy dependencies can be used to enable compilation:

CODE:
  1. <dependency org="ant" name="ant" rev="1.6.5" />
  2. <dependency org="tapestry" name="tapestry" rev="4.0.2"/>
  3. <dependency org="org.eclipse.jdt" name="core" rev="3.1.1"/>

This also removed the OS dependency from gwt-dev, so the same jar file can be used for all operating systems.

 Playing it safe: Choosing database engines

  • December 13th, 2007
  • 11:32 pm

The project I'm working on now is using Oracle for data storage. Not only that, it's good old 9.2. Try installing that on your brand new Ubuntu. Oh well, it's doable, but recently we've begun discussing if we could run it on MySQL or PostgreSQL (I'm voting for PostgreSQL, but that's mostly because I used MySQL back when foreign keys and other advanced features weren't exactly implemented, and in the meantime, I've come to like PostgreSQL. At the very least, hitting Ctrl-c in psql does not exit the prompt like mysql does).
The way it works now is that if something goes wrong, we can either call Oracle or our strategic Oracle partner, and they'll fix it. This is just about everything I'm against: paying large amounts of money just because then we can blame somebody else when things screw up.
There are basically two issues with switching to a more light-weight database, and they're probably more or less the same on many projects. First of all, performance. Oracle probably has a good advantage there, especially in handling large and complex queries. Secondly, maintenance, especially backup and failover.
The only one of these points which I think is valid is backup - it doesn't really help if it takes a week to backup or restore data. However, when that's said, we're left with two things: Performance and reliability. The Oracle way (I'm using this term as the broadest possible. Replace Oracle with MSSQL or DB2 if you like) is to add more memory, disk, clustering, high availability. Expensive, but you get to live in your little world where you can just write code against one large database.
The other way, which I prefer (and which is probably in the Web 2.0 spirit) is to distribute. Distribute both data and processing to a number of autonomous nodes which can operate independently of each other. This is no news, and has been done many times, but it's not something that's normally considered when building good old business applications.
The result of distributing is essentially that you think about how you're accessing your data. Instead of just delegating al of the work to the database and hoping for the best, you're actually forced to analyze data relationships to discover separate components. If this succeeds, the choice of database should no longer be about whether it can optimize a query over 50 tables with subselects, type conversions, views, functions, and other stuff, but if it is efficient at looking up simple data relations. My guess is that all the popular databases can do this, so then you're free to choose the cheapest, the one which is easiest to work with, or whatever suits your environment.

Returning to the project I'm working on, we have some reservations about some of the queries we're executing. They're on Oracle syntax right now, but can probably be converted to regular SQL in a finite amount of time. That doesn't make them any smaller, but we have a good amount of pretty static data (addresses, classifications, and so on), which are retrieved together with the more dynamic data. It the static data is removed from the SQL, we end up with some much simpler queries, which shouldn't be a problem for any database engine. The problem which remains is how the static data is retrieved effeciently. At the moment, I'm leaning towards a solution where we implement a service which can take a list of data keys and return the data. Depending on the amount of data, the service can then be implemented as a in-memory map, a memcached cache, or maybe even something like Hadoop. No matter what, basing the model on a basic principle of isolating static data from the dynamic, and only querying the dynamic data seems like the way to go as a first step - and as a nice side effect, the dependency on the database's ability to perform doesn't matter that much anymore.

This probably just sound like drunken ramblings to those who have actually implemented distributed business systems, but bear with me, it's a first for me, and I need to get things out of my head before the space runs out.

 GWT Xdoclet with typeArgs

  • December 4th, 2007
  • 11:14 pm

I've updated the GWT Xdoclet module to support typeArgs for collections. This is used when returning a collection from a service method:

CODE:
  1. /**
  2. * @gwt.serviceMethod
  3. * @gwt.typeArgs dk.contix.Test
  4. */
  5. public List getObjects() {
  6.   return new ArrayList();
  7. }

Notice that the object type is not written in <> as you would do if using GWT directly.

The new module can be downloaded here: xdoclet-gwt-module-0.5.jar.

 Software modeling

  • December 4th, 2007
  • 11:03 pm

I've been scheduled to do one of Trifork's Software Pilot JAOO meetups. It's a simple concept: we choose a topic, invite some speakers (or use one from Trifork), and people show to listen to and discuss various topics. It's free for all, so do join us. More information is available at trifork.com. The topic for the last meetup was application security, which Kresten presented, and the next time it will be about software modeling with focus on color modeling.

I find color modeling interesting because it's fun to work with, and it's very simple to learn and teach. Of course, it helps that it puts those colored post-its to use, but my experience also tells me that it's a good technique in a number of cases.

More on color modeling at the meetup. Right now, I'd like to reflect a little upon the project, I'm on at the moment. We're currently developing a new system for The Danish Medicines Agency which will be used for all medication ordinations in Denmark. Right now, if you're admitted to a hospital, you better be awake, because you're basically the only one who know what kind of medication you're on. If you can't tell the doctors yourself, there's no way to know what medication you've received recently. This is what the new system is trying to solve.

Now, the system is pretty simple, as it's basically a data repository accessible via web services. We receive medication data, store it, and send it out again when requested to do so. The web service interfaces were defined long before we started development, so that wasn't a concern to us (beyond using them). The usual (or enterprisey) way to proceed would be to

  • Install Axis or something alike
  • Generate code based on the WSDL and XSD files
  • Receive these types at the topmost layer and convert them to some kind of value objects or data transfer objects
  • Map the value objects to a database using Hibernate, EJB3 or another O/R mapper

Inspired by Steve Loughran and Edmund Smith's Rethinking the Java SOAP Stack, I suggested that we skip the code generation and value objects steps and just used XML and JDBC directly. This didn't exactly receive a warm welcome, but after discussing it more, everybody more or less agreed that the value of the generated code was not exactly clear. Also, there's been some trouble with Hibernate, especially in very large databases and when writing queries spanning a large number of tables with complex joins.

This means that the system is now based on basic JDBC (through Spring JDBC) and XML. The Java DOM API isn't exactly a pleasure to work with, so I wrote a pretty simple wrapper class for it, which can do something like this:

CODE:
  1. Namespaces ns = new Namespaces();
  2. ns.add("mc", "http://www.dkma.dk/medicinecard/xml.schema/2008.01.01");
  3.  
  4. XMLObject xml = new XMLObject();
  5. xml.setValue("mc:Card/mc:Patient/mc:Identifier", "identifier", ns);
  6.  
  7. // num will be null
  8. Long num = xml.getLong("Card/Element/Does/Not/Exist");
  9.  
  10. String id = xml.getString("Card/Patient/Identifier");

In other words, a very simple api for accessing XML structures. In setValue, elements are created automatically if they didn't exist, and the get methods will never throw a NullPointerException. Compare this with a similar DOM call where any number of NPEs can occur:

CODE:
  1. document.getFirstChild().getChildNodes().item(0).getChildNodes().item(0).getNodeValue();

How do we then ensure that we actually generate valid XML? The answer is heavy unit testing and schema validation. Just like you should do in any case.

We've now reached the point where we've implemented most of our web services, and for once, I don't have the feeling that I've spent my time writing stupid value classes with no functionality. The jury is still out on whether the design is actually good and scalable, but I think it's a good approach. Should everybody do it? Probably not. But it definitely shows that you shouldn't just accept common orthodoxy and do the usual enterprise system without reflecting upon how your model will be influenced.

By the way, this system will be released under an open source license (probably Apache 2.0 or Mozilla Public License 1.1) at Softwarebørsen, the Danish government's open source repository.