5765 |
29 Nov 19 |
nicklas |
1 |
package net.sf.basedb.reggie.grid; |
5765 |
29 Nov 19 |
nicklas |
2 |
|
5766 |
02 Dec 19 |
nicklas |
3 |
import java.io.StringWriter; |
5765 |
29 Nov 19 |
nicklas |
4 |
import java.util.Arrays; |
5766 |
02 Dec 19 |
nicklas |
5 |
import java.util.List; |
5766 |
02 Dec 19 |
nicklas |
6 |
import java.util.regex.Matcher; |
5766 |
02 Dec 19 |
nicklas |
7 |
import java.util.regex.Pattern; |
5765 |
29 Nov 19 |
nicklas |
8 |
|
5766 |
02 Dec 19 |
nicklas |
9 |
import net.sf.basedb.core.BioSource; |
5765 |
29 Nov 19 |
nicklas |
10 |
import net.sf.basedb.core.DbControl; |
5766 |
02 Dec 19 |
nicklas |
11 |
import net.sf.basedb.core.DerivedBioAssay; |
5766 |
02 Dec 19 |
nicklas |
12 |
import net.sf.basedb.core.InvalidDataException; |
5765 |
29 Nov 19 |
nicklas |
13 |
import net.sf.basedb.core.ItemList; |
5765 |
29 Nov 19 |
nicklas |
14 |
import net.sf.basedb.core.ItemNotFoundException; |
5765 |
29 Nov 19 |
nicklas |
15 |
import net.sf.basedb.core.ItemParameterType; |
5766 |
02 Dec 19 |
nicklas |
16 |
import net.sf.basedb.core.ItemQuery; |
5765 |
29 Nov 19 |
nicklas |
17 |
import net.sf.basedb.core.Job; |
5766 |
02 Dec 19 |
nicklas |
18 |
import net.sf.basedb.core.ProgressReporter; |
5765 |
29 Nov 19 |
nicklas |
19 |
import net.sf.basedb.core.SessionControl; |
5815 |
24 Jan 20 |
nicklas |
20 |
import net.sf.basedb.core.StringParameterType; |
5766 |
02 Dec 19 |
nicklas |
21 |
import net.sf.basedb.core.Type; |
5766 |
02 Dec 19 |
nicklas |
22 |
import net.sf.basedb.core.query.Expressions; |
5766 |
02 Dec 19 |
nicklas |
23 |
import net.sf.basedb.core.query.Hql; |
5766 |
02 Dec 19 |
nicklas |
24 |
import net.sf.basedb.core.query.Restrictions; |
5766 |
02 Dec 19 |
nicklas |
25 |
import net.sf.basedb.core.snapshot.SnapshotManager; |
5765 |
29 Nov 19 |
nicklas |
26 |
import net.sf.basedb.opengrid.JobDefinition; |
5772 |
03 Dec 19 |
nicklas |
27 |
import net.sf.basedb.opengrid.JobStatus; |
5765 |
29 Nov 19 |
nicklas |
28 |
import net.sf.basedb.opengrid.OpenGridCluster; |
5772 |
03 Dec 19 |
nicklas |
29 |
import net.sf.basedb.opengrid.OpenGridSession; |
5765 |
29 Nov 19 |
nicklas |
30 |
import net.sf.basedb.opengrid.ScriptBuilder; |
5765 |
29 Nov 19 |
nicklas |
31 |
import net.sf.basedb.opengrid.config.ClusterConfig; |
5765 |
29 Nov 19 |
nicklas |
32 |
import net.sf.basedb.opengrid.config.JobConfig; |
5766 |
02 Dec 19 |
nicklas |
33 |
import net.sf.basedb.opengrid.filetransfer.StringUploadSource; |
5772 |
03 Dec 19 |
nicklas |
34 |
import net.sf.basedb.opengrid.service.JobCompletionHandler; |
5765 |
29 Nov 19 |
nicklas |
35 |
import net.sf.basedb.reggie.Reggie; |
5765 |
29 Nov 19 |
nicklas |
36 |
import net.sf.basedb.reggie.XmlConfig; |
5766 |
02 Dec 19 |
nicklas |
37 |
import net.sf.basedb.reggie.dao.Annotationtype; |
5766 |
02 Dec 19 |
nicklas |
38 |
import net.sf.basedb.reggie.dao.Subtype; |
5765 |
29 Nov 19 |
nicklas |
39 |
import net.sf.basedb.util.Values; |
5766 |
02 Dec 19 |
nicklas |
40 |
import net.sf.basedb.util.export.TableWriter; |
5765 |
29 Nov 19 |
nicklas |
41 |
|
5765 |
29 Nov 19 |
nicklas |
42 |
/** |
5765 |
29 Nov 19 |
nicklas |
Helper class for generating variant |
5765 |
29 Nov 19 |
nicklas |
statistics script and send it to the cluster for execution. |
5765 |
29 Nov 19 |
nicklas |
45 |
|
5765 |
29 Nov 19 |
nicklas |
@author nicklas |
5765 |
29 Nov 19 |
nicklas |
@since 4.25 |
5765 |
29 Nov 19 |
nicklas |
48 |
*/ |
5765 |
29 Nov 19 |
nicklas |
49 |
public class VariantStatisticsJobCreator |
6674 |
11 Apr 22 |
nicklas |
50 |
extends AbstractJobCreator |
5765 |
29 Nov 19 |
nicklas |
51 |
{ |
5765 |
29 Nov 19 |
nicklas |
52 |
|
5815 |
24 Jan 20 |
nicklas |
53 |
private String outputBaseName = "scanb-tumors"; |
5765 |
29 Nov 19 |
nicklas |
54 |
|
5765 |
29 Nov 19 |
nicklas |
55 |
public VariantStatisticsJobCreator() |
5765 |
29 Nov 19 |
nicklas |
56 |
{} |
5765 |
29 Nov 19 |
nicklas |
57 |
|
5765 |
29 Nov 19 |
nicklas |
58 |
/** |
5815 |
24 Jan 20 |
nicklas |
Set the base name of files that are created. |
5815 |
24 Jan 20 |
nicklas |
Typically, this should be "scanb-tumors" or |
5815 |
24 Jan 20 |
nicklas |
"scanb-normals" (.vcf.gz is and other extensions are automatically appended) |
5815 |
24 Jan 20 |
nicklas |
62 |
*/ |
5815 |
24 Jan 20 |
nicklas |
63 |
public void setOutputBaseName(String baseName) |
5815 |
24 Jan 20 |
nicklas |
64 |
{ |
5815 |
24 Jan 20 |
nicklas |
65 |
this.outputBaseName = baseName; |
5815 |
24 Jan 20 |
nicklas |
66 |
} |
5815 |
24 Jan 20 |
nicklas |
67 |
|
5815 |
24 Jan 20 |
nicklas |
68 |
/** |
5765 |
29 Nov 19 |
nicklas |
Schedule a job on the cluster for collecting variant statistics. |
5765 |
29 Nov 19 |
nicklas |
@return The corresponding job in BASE |
5765 |
29 Nov 19 |
nicklas |
71 |
*/ |
5766 |
02 Dec 19 |
nicklas |
72 |
public Job createVariantStatisticsJob(DbControl dc, OpenGridCluster cluster, ItemList list, ProgressReporter progress) |
5765 |
29 Nov 19 |
nicklas |
73 |
{ |
5765 |
29 Nov 19 |
nicklas |
74 |
SessionControl sc = dc.getSessionControl(); |
5765 |
29 Nov 19 |
nicklas |
75 |
|
5765 |
29 Nov 19 |
nicklas |
76 |
ClusterConfig clusterCfg = cluster.getConfig(); |
5765 |
29 Nov 19 |
nicklas |
77 |
XmlConfig cfg = Reggie.getConfig(cluster.getId()); |
5765 |
29 Nov 19 |
nicklas |
78 |
if (cfg == null) |
5765 |
29 Nov 19 |
nicklas |
79 |
{ |
5765 |
29 Nov 19 |
nicklas |
80 |
throw new ItemNotFoundException("No configuration in reggie-config.xml for cluster: " + cluster.getId()); |
5765 |
29 Nov 19 |
nicklas |
81 |
} |
5765 |
29 Nov 19 |
nicklas |
82 |
|
5768 |
02 Dec 19 |
nicklas |
83 |
String projectRoot = cfg.getRequiredConfig("project-archive", null); |
5768 |
02 Dec 19 |
nicklas |
84 |
String pipeline_scripts_path = cfg.getRequiredConfig("programs/pipeline-scripts/path", null); |
5768 |
02 Dec 19 |
nicklas |
85 |
String bedtools_path = cfg.getRequiredConfig("programs/bedtools/path", null); |
5768 |
02 Dec 19 |
nicklas |
86 |
|
5765 |
29 Nov 19 |
nicklas |
// Options for the job |
5765 |
29 Nov 19 |
nicklas |
88 |
JobConfig jobConfig = new JobConfig(); |
5765 |
29 Nov 19 |
nicklas |
89 |
if (priority != null) jobConfig.setPriority(priority); |
6981 |
17 Jan 23 |
nicklas |
90 |
if (partition != null) jobConfig.setQsubOption("q", ScriptUtil.checkValidScriptParameter(partition)); |
6981 |
17 Jan 23 |
nicklas |
91 |
jobConfig.convertOptionsTo(cluster.getConfig().getType()); |
5765 |
29 Nov 19 |
nicklas |
92 |
|
5765 |
29 Nov 19 |
nicklas |
// Create job |
5765 |
29 Nov 19 |
nicklas |
94 |
Job statJob = Job.getNew(dc, null, null, null); |
5772 |
03 Dec 19 |
nicklas |
95 |
statJob.setItemSubtype(Subtype.VARIANT_STATISTICS_JOB.get(dc)); |
5765 |
29 Nov 19 |
nicklas |
96 |
statJob.setPluginVersion("reggie-"+Reggie.VERSION); |
5765 |
29 Nov 19 |
nicklas |
97 |
statJob.setSendMessage(Values.getBoolean(sc.getUserClientSetting("plugins.sendmessage"), false)); |
5765 |
29 Nov 19 |
nicklas |
98 |
statJob.setName("Build variant statistics"); |
5765 |
29 Nov 19 |
nicklas |
99 |
statJob.setParameterValue("list", new ItemParameterType<ItemList>(ItemList.class, null), list); |
5815 |
24 Jan 20 |
nicklas |
100 |
statJob.setParameterValue("outputBaseName", new StringParameterType(), outputBaseName); |
5765 |
29 Nov 19 |
nicklas |
101 |
if (debug) statJob.setName(statJob.getName() + " (debug)"); |
6981 |
17 Jan 23 |
nicklas |
102 |
if (partition != null) statJob.setParameterValue("partition", new StringParameterType(), partition); |
5765 |
29 Nov 19 |
nicklas |
103 |
dc.saveItem(statJob); |
5765 |
29 Nov 19 |
nicklas |
104 |
|
5765 |
29 Nov 19 |
nicklas |
105 |
ScriptBuilder script = new ScriptBuilder(); |
6665 |
05 Apr 22 |
nicklas |
106 |
script.cmd(debug ? "set -ex" : "set -e"); |
5765 |
29 Nov 19 |
nicklas |
107 |
script.comment("Setting up scripting environment and copying script to tmp folder"); |
5768 |
02 Dec 19 |
nicklas |
108 |
script.cmd("export ScriptDir=" + pipeline_scripts_path); |
5815 |
24 Jan 20 |
nicklas |
109 |
script.cmd("OUTNAME="+outputBaseName); |
5768 |
02 Dec 19 |
nicklas |
110 |
script.newLine(); |
5768 |
02 Dec 19 |
nicklas |
111 |
|
5768 |
02 Dec 19 |
nicklas |
112 |
script.cmd("cd ${TMPDIR}"); |
5768 |
02 Dec 19 |
nicklas |
113 |
script.cmd("cp ${ScriptDir}/mut_stats.py ."); |
5773 |
03 Dec 19 |
nicklas |
114 |
script.cmd("mkdir results"); |
5773 |
03 Dec 19 |
nicklas |
115 |
script.cmd("mkdir tmp"); |
5768 |
02 Dec 19 |
nicklas |
116 |
script.newLine(); |
5765 |
29 Nov 19 |
nicklas |
117 |
|
5768 |
02 Dec 19 |
nicklas |
118 |
String statCmd = "./mut_stats.py"; |
5768 |
02 Dec 19 |
nicklas |
119 |
statCmd += " ${WD}/vcflist.txt"; |
5770 |
03 Dec 19 |
nicklas |
120 |
statCmd += " ${WD}/progress"; |
5815 |
24 Jan 20 |
nicklas |
121 |
statCmd += " ${WD}/${OUTNAME}.log"; |
5815 |
24 Jan 20 |
nicklas |
122 |
statCmd += " > tmp/${OUTNAME}.vcf"; |
5768 |
02 Dec 19 |
nicklas |
123 |
script.cmd(statCmd); |
5772 |
03 Dec 19 |
nicklas |
124 |
script.progress(95, "Sorting and indexing..."); |
5815 |
24 Jan 20 |
nicklas |
125 |
script.cmd(bedtools_path+" sort -header -i tmp/${OUTNAME}.vcf | bgzip -c > results/${OUTNAME}.vcf.gz"); |
5815 |
24 Jan 20 |
nicklas |
126 |
script.cmd("tabix results/${OUTNAME}.vcf.gz"); |
5773 |
03 Dec 19 |
nicklas |
127 |
script.cmd("cp results/* ${WD}"); |
5765 |
29 Nov 19 |
nicklas |
128 |
|
5766 |
02 Dec 19 |
nicklas |
script.progress(99, "Cleaning up temporary folders"); |
5766 |
02 Dec 19 |
nicklas |
130 |
|
5766 |
02 Dec 19 |
nicklas |
// Export a list with PATIENT name and path to RAW VCF file |
5766 |
02 Dec 19 |
nicklas |
StringWriter vcfList = new StringWriter(list.getSize()*80); |
5766 |
02 Dec 19 |
nicklas |
TableWriter tw = new TableWriter(vcfList); |
5766 |
02 Dec 19 |
nicklas |
// For each alignment we need to find the patient. We design a query |
5766 |
02 Dec 19 |
nicklas |
// to load it from the specimen name which we get by cutting the front |
5766 |
02 Dec 19 |
nicklas |
// of the alignment name |
5766 |
02 Dec 19 |
nicklas |
ItemQuery<BioSource> findPatBySpecimen = BioSource.getQuery(); |
5766 |
02 Dec 19 |
nicklas |
Subtype.PATIENT.addFilter(dc, findPatBySpecimen); |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.setIncludes(Reggie.INCLUDE_IN_CURRENT_PROJECT); |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.join(Hql.innerJoin("childCreationEvents", "cce")); |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.join(Hql.innerJoin("cce", "event", "evt")); |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.join(Hql.innerJoin("evt", "bioMaterial", "cse")); // 'cse' should now reference a case |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.join(Hql.innerJoin("cse", "childCreationEvents", "cce2")); |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.join(Hql.innerJoin("cce2", "event", "evt2")); |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.join(Hql.innerJoin("evt2", "bioMaterial", "spm")); // 'spm' should now reference a specimen/nospecimen |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.restrict(Restrictions.eq(Hql.property("spm", "name"), Expressions.parameter("specimen"))); |
5765 |
29 Nov 19 |
nicklas |
147 |
|
6183 |
26 Mar 21 |
nicklas |
ItemQuery<DerivedBioAssay> query = list.getMembers(); |
5766 |
02 Dec 19 |
nicklas |
query.setIncludes(Reggie.INCLUDE_IN_CURRENT_PROJECT); |
5766 |
02 Dec 19 |
nicklas |
List<DerivedBioAssay> alignments = query.list(dc); |
5765 |
29 Nov 19 |
nicklas |
151 |
|
5766 |
02 Dec 19 |
nicklas |
SnapshotManager manager = new SnapshotManager(); |
5766 |
02 Dec 19 |
nicklas |
int count = 0; |
5766 |
02 Dec 19 |
nicklas |
int total = alignments.size(); |
5766 |
02 Dec 19 |
nicklas |
Pattern specimenName = Pattern.compile("(\\d+\\.\\d+)\\..*"); // Extract specimen name by taking first part of alignment name |
5770 |
03 Dec 19 |
nicklas |
156 |
|
5766 |
02 Dec 19 |
nicklas |
for (DerivedBioAssay alignment : alignments) |
5766 |
02 Dec 19 |
nicklas |
158 |
{ |
5766 |
02 Dec 19 |
nicklas |
count++; |
5766 |
02 Dec 19 |
nicklas |
if (progress != null && count % 100 == 0) |
5766 |
02 Dec 19 |
nicklas |
161 |
{ |
5766 |
02 Dec 19 |
nicklas |
progress.display(count * 90 / total, "Exporting Patient and VCF list (" + count + " of " + total + ")..."); |
5766 |
02 Dec 19 |
nicklas |
163 |
} |
5766 |
02 Dec 19 |
nicklas |
164 |
|
5766 |
02 Dec 19 |
nicklas |
Matcher m = specimenName.matcher(alignment.getName()); |
5766 |
02 Dec 19 |
nicklas |
if (!m.matches()) |
5766 |
02 Dec 19 |
nicklas |
167 |
{ |
5766 |
02 Dec 19 |
nicklas |
throw new InvalidDataException("Could not get specimen name for alignment: "+ alignment.getName()); |
5766 |
02 Dec 19 |
nicklas |
169 |
} |
5766 |
02 Dec 19 |
nicklas |
170 |
|
5766 |
02 Dec 19 |
nicklas |
String specimen = m.group(1); |
5766 |
02 Dec 19 |
nicklas |
findPatBySpecimen.setParameter("specimen", specimen, Type.STRING); |
5766 |
02 Dec 19 |
nicklas |
List<BioSource> patients = findPatBySpecimen.list(dc); |
5766 |
02 Dec 19 |
nicklas |
if (patients.size() != 1) |
5766 |
02 Dec 19 |
nicklas |
175 |
{ |
5766 |
02 Dec 19 |
nicklas |
if (patients.size() == 0) throw new ItemNotFoundException("Could not find a patient item for alignment: " +alignment.getName()); |
5766 |
02 Dec 19 |
nicklas |
throw new InvalidDataException("Found "+ patients.size() + " patient items for alignment: " + alignment.getName()); |
5766 |
02 Dec 19 |
nicklas |
178 |
} |
5766 |
02 Dec 19 |
nicklas |
String patient = patients.get(0).getName(); |
5768 |
02 Dec 19 |
nicklas |
String dataFolder = ScriptUtil.checkValidPath((String)Annotationtype.DATA_FILES_FOLDER.getAnnotationValue(dc, manager, alignment), true, true); |
5770 |
03 Dec 19 |
nicklas |
tw.tablePrintData(patient, projectRoot+dataFolder+"/variants-raw.vcf.gz", alignment.getName()); |
5766 |
02 Dec 19 |
nicklas |
182 |
} |
5766 |
02 Dec 19 |
nicklas |
tw.flush(); |
5766 |
02 Dec 19 |
nicklas |
tw.close(); |
5766 |
02 Dec 19 |
nicklas |
185 |
|
6674 |
11 Apr 22 |
nicklas |
JobDefinition jobDef = new JobDefinition("VariantStatistics", jobConfig, batchConfig, statJob); |
5765 |
29 Nov 19 |
nicklas |
jobDef.setDebug(debug); |
5766 |
02 Dec 19 |
nicklas |
jobDef.addFile(new StringUploadSource("vcflist.txt", vcfList.toString())); |
5765 |
29 Nov 19 |
nicklas |
jobDef.setCmd(script.toString()); |
5765 |
29 Nov 19 |
nicklas |
190 |
|
5765 |
29 Nov 19 |
nicklas |
ScriptUtil.submitJobs(dc, cluster, Arrays.asList(jobDef)); |
5765 |
29 Nov 19 |
nicklas |
return statJob; |
5765 |
29 Nov 19 |
nicklas |
193 |
} |
5765 |
29 Nov 19 |
nicklas |
194 |
|
5772 |
03 Dec 19 |
nicklas |
public static class VariantStatisticsJobCompletionHandler |
5772 |
03 Dec 19 |
nicklas |
implements JobCompletionHandler |
5772 |
03 Dec 19 |
nicklas |
197 |
{ |
5772 |
03 Dec 19 |
nicklas |
198 |
|
5772 |
03 Dec 19 |
nicklas |
public VariantStatisticsJobCompletionHandler() |
5772 |
03 Dec 19 |
nicklas |
200 |
{} |
5765 |
29 Nov 19 |
nicklas |
201 |
|
5772 |
03 Dec 19 |
nicklas |
@Override |
5772 |
03 Dec 19 |
nicklas |
public String jobCompleted(SessionControl sc, OpenGridSession session, Job job, JobStatus status) |
5772 |
03 Dec 19 |
nicklas |
204 |
{ |
5772 |
03 Dec 19 |
nicklas |
String jobName = status.getName(); |
5815 |
24 Jan 20 |
nicklas |
String baseFileName = job.getParameterValue("outputBaseName"); |
5815 |
24 Jan 20 |
nicklas |
String log = session.getJobFileAsString(jobName, baseFileName+".log", "UTF-8"); |
5772 |
03 Dec 19 |
nicklas |
Stats stat = Stats.parse(log); |
5772 |
03 Dec 19 |
nicklas |
209 |
|
5772 |
03 Dec 19 |
nicklas |
return "Parsed " + stat.numVcf + " VCF files for " + stat.numPatients + " patients." + |
5773 |
03 Dec 19 |
nicklas |
" Found "+Reggie.formatCount(stat.numVariants) + " variants." + |
5815 |
24 Jan 20 |
nicklas |
" Result files can be found at: " + session.getHost().getWorkFolder(jobName) + |
5815 |
24 Jan 20 |
nicklas |
"/" + baseFileName + ".*"; |
5772 |
03 Dec 19 |
nicklas |
214 |
} |
5772 |
03 Dec 19 |
nicklas |
215 |
} |
5772 |
03 Dec 19 |
nicklas |
216 |
|
5772 |
03 Dec 19 |
nicklas |
static class Stats |
5772 |
03 Dec 19 |
nicklas |
218 |
{ |
5772 |
03 Dec 19 |
nicklas |
int numVcf = 0; |
5772 |
03 Dec 19 |
nicklas |
int numPatients = 0; |
5772 |
03 Dec 19 |
nicklas |
int numVariants = 0; |
5772 |
03 Dec 19 |
nicklas |
222 |
|
5772 |
03 Dec 19 |
nicklas |
static Stats parse(String statsOut) |
5772 |
03 Dec 19 |
nicklas |
224 |
{ |
5772 |
03 Dec 19 |
nicklas |
Stats s = new Stats(); |
5772 |
03 Dec 19 |
nicklas |
for (String line : statsOut.split("\n")) |
5772 |
03 Dec 19 |
nicklas |
227 |
{ |
5772 |
03 Dec 19 |
nicklas |
String[] stat = line.split(":", 2); |
5772 |
03 Dec 19 |
nicklas |
String key = stat[0].strip(); |
5772 |
03 Dec 19 |
nicklas |
int val = Values.getInt(stat[1].strip()); |
5772 |
03 Dec 19 |
nicklas |
if ("NumberOfVCF".equals(key)) |
5772 |
03 Dec 19 |
nicklas |
232 |
{ |
5772 |
03 Dec 19 |
nicklas |
s.numVcf = val; |
5772 |
03 Dec 19 |
nicklas |
234 |
} |
5772 |
03 Dec 19 |
nicklas |
else if ("NumberOfPatients".equals(key)) |
5772 |
03 Dec 19 |
nicklas |
236 |
{ |
5772 |
03 Dec 19 |
nicklas |
s.numPatients = val; |
5772 |
03 Dec 19 |
nicklas |
238 |
} |
5772 |
03 Dec 19 |
nicklas |
else if ("NumberOfVariants".equals(key)) |
5772 |
03 Dec 19 |
nicklas |
240 |
{ |
5772 |
03 Dec 19 |
nicklas |
s.numVariants = val; |
5772 |
03 Dec 19 |
nicklas |
242 |
} |
5772 |
03 Dec 19 |
nicklas |
243 |
} |
5772 |
03 Dec 19 |
nicklas |
return s; |
5772 |
03 Dec 19 |
nicklas |
245 |
} |
5772 |
03 Dec 19 |
nicklas |
246 |
} |
5765 |
29 Nov 19 |
nicklas |
247 |
} |