1091 |
27 May 09 |
nicklas |
1 |
package net.sf.basedb.genepattern.export; |
1091 |
27 May 09 |
nicklas |
2 |
|
1091 |
27 May 09 |
nicklas |
3 |
import java.sql.SQLException; |
1091 |
27 May 09 |
nicklas |
4 |
import java.util.Arrays; |
1092 |
27 May 09 |
nicklas |
5 |
import java.util.HashSet; |
1091 |
27 May 09 |
nicklas |
6 |
import java.util.List; |
1092 |
27 May 09 |
nicklas |
7 |
import java.util.Set; |
1091 |
27 May 09 |
nicklas |
8 |
|
1091 |
27 May 09 |
nicklas |
9 |
import net.sf.basedb.core.BioAssay; |
1091 |
27 May 09 |
nicklas |
10 |
import net.sf.basedb.core.BioAssaySet; |
1091 |
27 May 09 |
nicklas |
11 |
import net.sf.basedb.core.DatabaseException; |
1091 |
27 May 09 |
nicklas |
12 |
import net.sf.basedb.core.DbControl; |
1091 |
27 May 09 |
nicklas |
13 |
import net.sf.basedb.core.DynamicResultIterator; |
1091 |
27 May 09 |
nicklas |
14 |
import net.sf.basedb.core.DynamicSpotQuery; |
1091 |
27 May 09 |
nicklas |
15 |
import net.sf.basedb.core.IntensityTransform; |
1091 |
27 May 09 |
nicklas |
16 |
import net.sf.basedb.core.query.Expressions; |
1091 |
27 May 09 |
nicklas |
17 |
import net.sf.basedb.core.query.SqlResult; |
1091 |
27 May 09 |
nicklas |
18 |
import net.sf.basedb.util.export.TableWriter; |
1091 |
27 May 09 |
nicklas |
19 |
import net.sf.basedb.util.export.spotdata.AbstractBioAssaySetExporter; |
1091 |
27 May 09 |
nicklas |
20 |
import net.sf.basedb.util.export.spotdata.DynamicField; |
1091 |
27 May 09 |
nicklas |
21 |
import net.sf.basedb.util.export.spotdata.ExportableFieldFactory; |
1091 |
27 May 09 |
nicklas |
22 |
|
1091 |
27 May 09 |
nicklas |
23 |
/** |
1091 |
27 May 09 |
nicklas |
Bioassay set exporter implementation that exports spot data to |
1091 |
27 May 09 |
nicklas |
a GenePatter GCT file. The exported |
1091 |
27 May 09 |
nicklas |
file contains the following information: |
1091 |
27 May 09 |
nicklas |
<ul> |
1091 |
27 May 09 |
nicklas |
<li>Intensity values: The ch1 value for one-channel experiments and the |
1091 |
27 May 09 |
nicklas |
ratio for two-channel experiments. If the source bioassay set store |
1091 |
27 May 09 |
nicklas |
logged values, the output is also logged values. |
1091 |
27 May 09 |
nicklas |
<li>Reporter values: externalId and one additional column (default is |
1091 |
27 May 09 |
nicklas |
the description) |
1091 |
27 May 09 |
nicklas |
</ul> |
1091 |
27 May 09 |
nicklas |
<p> |
1091 |
27 May 09 |
nicklas |
Before use the following properties must be set: |
1091 |
27 May 09 |
nicklas |
<ul> |
1091 |
27 May 09 |
nicklas |
<li>{@link #setDbControl(DbControl)}: A DbControl is needed to extract |
1091 |
27 May 09 |
nicklas |
information from the database. |
1091 |
27 May 09 |
nicklas |
<li>{@link #setSource(BioAssaySet)}: The source bioassay set that we |
1091 |
27 May 09 |
nicklas |
should export data from. |
1091 |
27 May 09 |
nicklas |
<li>{@link #setWriter(TableWriter)}: The destination that we should write |
1091 |
27 May 09 |
nicklas |
the data to. |
1091 |
27 May 09 |
nicklas |
</ul> |
1091 |
27 May 09 |
nicklas |
<p> |
1091 |
27 May 09 |
nicklas |
The exporter can only be used once. A new instance is needed for each bioassay |
1091 |
27 May 09 |
nicklas |
set. |
1091 |
27 May 09 |
nicklas |
47 |
|
1091 |
27 May 09 |
nicklas |
@author nicklas |
1091 |
27 May 09 |
nicklas |
@since 1.0 |
1091 |
27 May 09 |
nicklas |
50 |
*/ |
1091 |
27 May 09 |
nicklas |
51 |
public class GctExporter |
1091 |
27 May 09 |
nicklas |
52 |
extends AbstractBioAssaySetExporter |
1091 |
27 May 09 |
nicklas |
53 |
{ |
1091 |
27 May 09 |
nicklas |
54 |
private TableWriter out; |
1091 |
27 May 09 |
nicklas |
55 |
private long spotCount; |
1091 |
27 May 09 |
nicklas |
56 |
public DynamicField description; |
1091 |
27 May 09 |
nicklas |
57 |
|
1091 |
27 May 09 |
nicklas |
58 |
/** |
1091 |
27 May 09 |
nicklas |
Create a new GCT exporter. |
1091 |
27 May 09 |
nicklas |
60 |
*/ |
1091 |
27 May 09 |
nicklas |
61 |
public GctExporter() |
1091 |
27 May 09 |
nicklas |
62 |
{} |
1091 |
27 May 09 |
nicklas |
63 |
|
1091 |
27 May 09 |
nicklas |
64 |
/* |
1091 |
27 May 09 |
nicklas |
More configuration options |
1091 |
27 May 09 |
nicklas |
66 |
*/ |
1091 |
27 May 09 |
nicklas |
67 |
/** |
1091 |
27 May 09 |
nicklas |
Set the stream were the exported data should be written. |
1091 |
27 May 09 |
nicklas |
It is expected that the given writer is a fresh writer and |
1091 |
27 May 09 |
nicklas |
that no data has been written to it yet. |
1091 |
27 May 09 |
nicklas |
71 |
*/ |
1091 |
27 May 09 |
nicklas |
72 |
public void setWriter(TableWriter out) |
1091 |
27 May 09 |
nicklas |
73 |
{ |
1091 |
27 May 09 |
nicklas |
74 |
this.out = out; |
1091 |
27 May 09 |
nicklas |
75 |
} |
1091 |
27 May 09 |
nicklas |
76 |
public void setDescriptionField(DynamicField field) |
1091 |
27 May 09 |
nicklas |
77 |
{ |
1091 |
27 May 09 |
nicklas |
78 |
this.description = field; |
1091 |
27 May 09 |
nicklas |
79 |
} |
1091 |
27 May 09 |
nicklas |
80 |
// -------------------------- |
1091 |
27 May 09 |
nicklas |
81 |
|
1091 |
27 May 09 |
nicklas |
82 |
/** |
1091 |
27 May 09 |
nicklas |
Get the writer that writes the data to the file. |
1091 |
27 May 09 |
nicklas |
84 |
*/ |
1091 |
27 May 09 |
nicklas |
85 |
protected TableWriter getTableWriter() |
1091 |
27 May 09 |
nicklas |
86 |
{ |
1091 |
27 May 09 |
nicklas |
87 |
return out; |
1091 |
27 May 09 |
nicklas |
88 |
} |
1091 |
27 May 09 |
nicklas |
89 |
|
1091 |
27 May 09 |
nicklas |
90 |
/* |
1091 |
27 May 09 |
nicklas |
From AbstractBioAssaySetExporter class |
1091 |
27 May 09 |
nicklas |
92 |
--------------------------------------- |
1091 |
27 May 09 |
nicklas |
93 |
*/ |
1091 |
27 May 09 |
nicklas |
94 |
/** |
1091 |
27 May 09 |
nicklas |
Prepare the export by pre-loading some information and |
1091 |
27 May 09 |
nicklas |
configure the queries that we are going to use. |
1091 |
27 May 09 |
nicklas |
97 |
*/ |
1091 |
27 May 09 |
nicklas |
98 |
@Override |
1091 |
27 May 09 |
nicklas |
99 |
protected void beginExport() |
1091 |
27 May 09 |
nicklas |
100 |
{ |
1091 |
27 May 09 |
nicklas |
101 |
super.beginExport(); |
1091 |
27 May 09 |
nicklas |
102 |
setAverageOnReporter(true); |
1091 |
27 May 09 |
nicklas |
103 |
addReporterFields(); |
1091 |
27 May 09 |
nicklas |
104 |
addSpotFields(); |
1091 |
27 May 09 |
nicklas |
105 |
setProgress(0, "Caching reporter data..."); |
1091 |
27 May 09 |
nicklas |
106 |
cacheReporterData(); |
1091 |
27 May 09 |
nicklas |
107 |
} |
1091 |
27 May 09 |
nicklas |
108 |
|
1091 |
27 May 09 |
nicklas |
109 |
/** |
1091 |
27 May 09 |
nicklas |
Writes headers and assay annotations. |
1091 |
27 May 09 |
nicklas |
@return TRUE to continue with the data export |
1091 |
27 May 09 |
nicklas |
112 |
*/ |
1091 |
27 May 09 |
nicklas |
113 |
@Override |
1091 |
27 May 09 |
nicklas |
114 |
protected boolean exportGlobalHeader() |
1091 |
27 May 09 |
nicklas |
115 |
{ |
1091 |
27 May 09 |
nicklas |
// Get configuration options |
1091 |
27 May 09 |
nicklas |
117 |
DbControl dc = getDbControl(); |
1091 |
27 May 09 |
nicklas |
118 |
BioAssaySet source = getSource(); |
1091 |
27 May 09 |
nicklas |
119 |
List<BioAssay> assays = getBioAssays(); |
1091 |
27 May 09 |
nicklas |
120 |
List<DynamicField> reporterFields = getReporterFields(); |
1091 |
27 May 09 |
nicklas |
121 |
|
1091 |
27 May 09 |
nicklas |
// First line is always: #1.2 |
1091 |
27 May 09 |
nicklas |
123 |
out.println("#1.2"); |
1091 |
27 May 09 |
nicklas |
124 |
|
1091 |
27 May 09 |
nicklas |
// Second line is: <number of reporters><tab><number of assays> |
1091 |
27 May 09 |
nicklas |
126 |
out.println(source.getNumReporters() + "\t" + assays.size()); |
1091 |
27 May 09 |
nicklas |
127 |
|
1091 |
27 May 09 |
nicklas |
// Third line is data header: NAME<tab>Description<tab> + one column for each assay |
1091 |
27 May 09 |
nicklas |
129 |
Object[] data = new Object[reporterFields.size() + assays.size()]; |
1091 |
27 May 09 |
nicklas |
130 |
int index = 0; |
1091 |
27 May 09 |
nicklas |
131 |
|
1092 |
27 May 09 |
nicklas |
132 |
boolean hasUniqueNames = checkUniqueNames(assays); |
1092 |
27 May 09 |
nicklas |
133 |
|
1091 |
27 May 09 |
nicklas |
// Reporter fields |
1091 |
27 May 09 |
nicklas |
135 |
for (DynamicField field : reporterFields) |
1091 |
27 May 09 |
nicklas |
136 |
{ |
1091 |
27 May 09 |
nicklas |
137 |
data[index++] = field.getTitle(); |
1091 |
27 May 09 |
nicklas |
138 |
} |
1091 |
27 May 09 |
nicklas |
// ... and one column for each assay |
1091 |
27 May 09 |
nicklas |
140 |
for (BioAssay ba : assays) |
1091 |
27 May 09 |
nicklas |
141 |
{ |
1092 |
27 May 09 |
nicklas |
142 |
data[index++] = hasUniqueNames ? ba.getName() : ba.getName() + "-" + ba.getId(); |
1091 |
27 May 09 |
nicklas |
143 |
} |
1091 |
27 May 09 |
nicklas |
144 |
out.tablePrintData(data); |
1091 |
27 May 09 |
nicklas |
145 |
|
1091 |
27 May 09 |
nicklas |
// This concludes the header section |
1091 |
27 May 09 |
nicklas |
147 |
out.flush(); |
1091 |
27 May 09 |
nicklas |
148 |
return true; |
1091 |
27 May 09 |
nicklas |
149 |
} |
1091 |
27 May 09 |
nicklas |
150 |
|
1091 |
27 May 09 |
nicklas |
151 |
/** |
1091 |
27 May 09 |
nicklas |
Writes spot data. |
1091 |
27 May 09 |
nicklas |
153 |
*/ |
1091 |
27 May 09 |
nicklas |
154 |
@Override |
1091 |
27 May 09 |
nicklas |
155 |
protected void exportSectionData() |
1091 |
27 May 09 |
nicklas |
156 |
{ |
1091 |
27 May 09 |
nicklas |
// Get configuration settings |
1091 |
27 May 09 |
nicklas |
158 |
DbControl dc = getDbControl(); |
1091 |
27 May 09 |
nicklas |
159 |
BioAssaySet source = getSource(); |
1091 |
27 May 09 |
nicklas |
160 |
List<DynamicField> reporterFields = getReporterFields(); |
1091 |
27 May 09 |
nicklas |
161 |
int numSpotFields = getSpotFields().size(); |
1091 |
27 May 09 |
nicklas |
162 |
List<BioAssay> assays = getBioAssays(); |
1091 |
27 May 09 |
nicklas |
163 |
IntensityTransform transform = source.getIntensityTransform(); |
1091 |
27 May 09 |
nicklas |
164 |
boolean isLogged = transform == IntensityTransform.LOG10 || |
1091 |
27 May 09 |
nicklas |
165 |
transform == IntensityTransform.LOG2; |
1091 |
27 May 09 |
nicklas |
166 |
spotCount = source.getNumSpots(); |
1091 |
27 May 09 |
nicklas |
167 |
|
1091 |
27 May 09 |
nicklas |
// Prepare the query and more |
1091 |
27 May 09 |
nicklas |
169 |
DynamicSpotQuery spotQuery = getSpotQuery(false); |
1091 |
27 May 09 |
nicklas |
170 |
prepareAssayIndexMap(assays, reporterFields.size(), 1); |
1091 |
27 May 09 |
nicklas |
171 |
Object[] data = new Object[reporterFields.size() + assays.size()]; |
1091 |
27 May 09 |
nicklas |
172 |
int posIndex = numSpotFields + 1; |
1091 |
27 May 09 |
nicklas |
173 |
int colIndex = numSpotFields + 2; |
1091 |
27 May 09 |
nicklas |
174 |
int currentPosition = -1; |
1091 |
27 May 09 |
nicklas |
175 |
|
1091 |
27 May 09 |
nicklas |
176 |
setProgress(10, "Preparing to load spot data..."); |
1091 |
27 May 09 |
nicklas |
177 |
DynamicResultIterator it = spotQuery.iterate(dc); |
1091 |
27 May 09 |
nicklas |
178 |
try |
1091 |
27 May 09 |
nicklas |
179 |
{ |
1091 |
27 May 09 |
nicklas |
180 |
long numDone = 0; |
1091 |
27 May 09 |
nicklas |
181 |
int numLines = 0; |
1091 |
27 May 09 |
nicklas |
182 |
int progressInterval = 10; // Every ten lines |
1091 |
27 May 09 |
nicklas |
183 |
|
1091 |
27 May 09 |
nicklas |
184 |
while (it.hasNext()) |
1091 |
27 May 09 |
nicklas |
185 |
{ |
1091 |
27 May 09 |
nicklas |
186 |
checkInterrupted(); |
1091 |
27 May 09 |
nicklas |
187 |
SqlResult result = it.next(); |
1091 |
27 May 09 |
nicklas |
188 |
++numDone; |
1091 |
27 May 09 |
nicklas |
189 |
|
1091 |
27 May 09 |
nicklas |
190 |
int position = result.getInt(posIndex); |
1091 |
27 May 09 |
nicklas |
191 |
if (position != currentPosition) |
1091 |
27 May 09 |
nicklas |
192 |
{ |
1091 |
27 May 09 |
nicklas |
193 |
if (currentPosition != -1) |
1091 |
27 May 09 |
nicklas |
194 |
{ |
1091 |
27 May 09 |
nicklas |
// Write the current data line when the position changes |
1091 |
27 May 09 |
nicklas |
196 |
++numLines; |
1091 |
27 May 09 |
nicklas |
197 |
out.tablePrintData(data); |
1091 |
27 May 09 |
nicklas |
198 |
Arrays.fill(data, null); |
1091 |
27 May 09 |
nicklas |
199 |
if (progressInterval % numLines == 0) |
1091 |
27 May 09 |
nicklas |
200 |
{ |
1091 |
27 May 09 |
nicklas |
201 |
int percent = 10+(int)((90L * numDone) / spotCount); |
1091 |
27 May 09 |
nicklas |
202 |
setProgress(percent, "Exporting spot data: " + |
1091 |
27 May 09 |
nicklas |
203 |
numDone + " of " + spotCount + " done"); |
1091 |
27 May 09 |
nicklas |
204 |
} |
1091 |
27 May 09 |
nicklas |
205 |
} |
1091 |
27 May 09 |
nicklas |
// Prepare the next data line by copying the cached reporter fields |
1091 |
27 May 09 |
nicklas |
207 |
copyReporterFields(position, data, 0); |
1091 |
27 May 09 |
nicklas |
208 |
currentPosition = position; |
1091 |
27 May 09 |
nicklas |
209 |
} |
1091 |
27 May 09 |
nicklas |
210 |
|
1091 |
27 May 09 |
nicklas |
// For 1-channel data, get the ch1 intensity... |
1091 |
27 May 09 |
nicklas |
212 |
double ch1 = result.getFloat(1); |
1091 |
27 May 09 |
nicklas |
213 |
boolean goodValue = true; |
1091 |
27 May 09 |
nicklas |
214 |
if (numSpotFields == 2) |
1091 |
27 May 09 |
nicklas |
215 |
{ |
1091 |
27 May 09 |
nicklas |
// For 2-channel data, get the ch1/ch2 ratio... |
1091 |
27 May 09 |
nicklas |
217 |
double ch2 = result.getFloat(2); |
1091 |
27 May 09 |
nicklas |
218 |
if (isLogged) |
1091 |
27 May 09 |
nicklas |
219 |
{ |
1091 |
27 May 09 |
nicklas |
220 |
ch1 -= ch2; |
1091 |
27 May 09 |
nicklas |
221 |
} |
1091 |
27 May 09 |
nicklas |
222 |
else if (ch2 != 0) |
1091 |
27 May 09 |
nicklas |
223 |
{ |
1091 |
27 May 09 |
nicklas |
224 |
ch1 /= ch2; |
1091 |
27 May 09 |
nicklas |
225 |
if (ch1 <= 0) goodValue = false; |
1091 |
27 May 09 |
nicklas |
226 |
} |
1091 |
27 May 09 |
nicklas |
227 |
else |
1091 |
27 May 09 |
nicklas |
228 |
{ |
1091 |
27 May 09 |
nicklas |
229 |
goodValue = false; |
1091 |
27 May 09 |
nicklas |
230 |
} |
1091 |
27 May 09 |
nicklas |
231 |
} |
1091 |
27 May 09 |
nicklas |
232 |
|
1091 |
27 May 09 |
nicklas |
// Store result into 'data' array |
1091 |
27 May 09 |
nicklas |
234 |
short column = result.getShort(colIndex); |
1091 |
27 May 09 |
nicklas |
235 |
int index = getAssayIndex(column); |
1091 |
27 May 09 |
nicklas |
236 |
data[index] = goodValue ? ch1 : ""; |
1091 |
27 May 09 |
nicklas |
237 |
} |
1091 |
27 May 09 |
nicklas |
// Print the final line of data |
1091 |
27 May 09 |
nicklas |
239 |
out.tablePrintData(data); |
1091 |
27 May 09 |
nicklas |
240 |
out.flush(); |
1091 |
27 May 09 |
nicklas |
241 |
} |
1091 |
27 May 09 |
nicklas |
242 |
catch (SQLException ex) |
1091 |
27 May 09 |
nicklas |
243 |
{ |
1091 |
27 May 09 |
nicklas |
244 |
throw new DatabaseException(ex); |
1091 |
27 May 09 |
nicklas |
245 |
} |
1091 |
27 May 09 |
nicklas |
246 |
finally |
1091 |
27 May 09 |
nicklas |
247 |
{ |
1091 |
27 May 09 |
nicklas |
248 |
if (it != null) it.close(); |
1091 |
27 May 09 |
nicklas |
249 |
} |
1091 |
27 May 09 |
nicklas |
250 |
} |
1091 |
27 May 09 |
nicklas |
251 |
|
1091 |
27 May 09 |
nicklas |
252 |
@Override |
1091 |
27 May 09 |
nicklas |
253 |
protected void endExport(RuntimeException e) |
1091 |
27 May 09 |
nicklas |
254 |
{ |
1091 |
27 May 09 |
nicklas |
255 |
if (e == null) setProgress(100, "Export complete. " + spotCount + " spots done"); |
1091 |
27 May 09 |
nicklas |
256 |
out.flush(); |
1091 |
27 May 09 |
nicklas |
257 |
out = null; |
1091 |
27 May 09 |
nicklas |
258 |
super.endExport(e); |
1091 |
27 May 09 |
nicklas |
259 |
} |
1091 |
27 May 09 |
nicklas |
260 |
// ---------------------------------------- |
1091 |
27 May 09 |
nicklas |
261 |
|
1091 |
27 May 09 |
nicklas |
262 |
/** |
1091 |
27 May 09 |
nicklas |
Adds position, externalId, symbol, plus all admin-defined extended |
1091 |
27 May 09 |
nicklas |
properties as reporter fields. |
1091 |
27 May 09 |
nicklas |
265 |
*/ |
1091 |
27 May 09 |
nicklas |
266 |
protected void addReporterFields() |
1091 |
27 May 09 |
nicklas |
267 |
{ |
1091 |
27 May 09 |
nicklas |
268 |
addReporterField(ExportableFieldFactory.reporter("externalId", "NAME", null, null)); |
1091 |
27 May 09 |
nicklas |
269 |
if (description != null) |
1091 |
27 May 09 |
nicklas |
270 |
{ |
1091 |
27 May 09 |
nicklas |
271 |
addReporterField(description); |
1091 |
27 May 09 |
nicklas |
272 |
} |
1091 |
27 May 09 |
nicklas |
273 |
else |
1091 |
27 May 09 |
nicklas |
274 |
{ |
1091 |
27 May 09 |
nicklas |
275 |
addReporterField(ExportableFieldFactory.simple(Expressions.string(""), "Description", null, null)); |
1091 |
27 May 09 |
nicklas |
276 |
} |
1091 |
27 May 09 |
nicklas |
277 |
} |
1091 |
27 May 09 |
nicklas |
278 |
|
1091 |
27 May 09 |
nicklas |
279 |
/** |
1091 |
27 May 09 |
nicklas |
Adds channel data as spot fields. |
1091 |
27 May 09 |
nicklas |
281 |
*/ |
1091 |
27 May 09 |
nicklas |
282 |
protected void addSpotFields() |
1091 |
27 May 09 |
nicklas |
283 |
{ |
1091 |
27 May 09 |
nicklas |
284 |
BioAssaySet source = getSource(); |
1091 |
27 May 09 |
nicklas |
285 |
int channels = source.getRawDataType().getChannels(); |
1091 |
27 May 09 |
nicklas |
286 |
for (int i = 1; i <= channels; ++i) |
1091 |
27 May 09 |
nicklas |
287 |
{ |
1091 |
27 May 09 |
nicklas |
288 |
addSpotField(ExportableFieldFactory.channel(i, null, null)); |
1091 |
27 May 09 |
nicklas |
289 |
} |
1091 |
27 May 09 |
nicklas |
290 |
} |
1091 |
27 May 09 |
nicklas |
291 |
|
1092 |
27 May 09 |
nicklas |
292 |
/** |
1092 |
27 May 09 |
nicklas |
Check if all bioassay names are unique. |
1092 |
27 May 09 |
nicklas |
@return TRUE if the names are unique, FALSE otherwise |
1092 |
27 May 09 |
nicklas |
295 |
*/ |
1092 |
27 May 09 |
nicklas |
296 |
private boolean checkUniqueNames(List<BioAssay> assays) |
1092 |
27 May 09 |
nicklas |
297 |
{ |
1092 |
27 May 09 |
nicklas |
298 |
Set<String> names = new HashSet<String>(); |
1092 |
27 May 09 |
nicklas |
299 |
for (BioAssay ba : assays) |
1092 |
27 May 09 |
nicklas |
300 |
{ |
1092 |
27 May 09 |
nicklas |
301 |
if (names.contains(ba.getName())) return false; |
1092 |
27 May 09 |
nicklas |
302 |
names.add(ba.getName()); |
1092 |
27 May 09 |
nicklas |
303 |
} |
1092 |
27 May 09 |
nicklas |
304 |
return true; |
1092 |
27 May 09 |
nicklas |
305 |
} |
1092 |
27 May 09 |
nicklas |
306 |
|
1092 |
27 May 09 |
nicklas |
307 |
|
1091 |
27 May 09 |
nicklas |
308 |
} |