4553 |
28 Aug 17 |
nicklas |
1 |
package net.sf.basedb.relax.plugins; |
4553 |
28 Aug 17 |
nicklas |
2 |
|
5510 |
18 Jun 19 |
nicklas |
3 |
import java.io.IOException; |
5510 |
18 Jun 19 |
nicklas |
4 |
import java.io.OutputStream; |
4553 |
28 Aug 17 |
nicklas |
5 |
import java.io.OutputStreamWriter; |
5510 |
18 Jun 19 |
nicklas |
6 |
import java.nio.charset.StandardCharsets; |
4553 |
28 Aug 17 |
nicklas |
7 |
import java.util.ArrayList; |
4553 |
28 Aug 17 |
nicklas |
8 |
import java.util.Arrays; |
4553 |
28 Aug 17 |
nicklas |
9 |
import java.util.Collections; |
4553 |
28 Aug 17 |
nicklas |
10 |
import java.util.HashMap; |
4553 |
28 Aug 17 |
nicklas |
11 |
import java.util.HashSet; |
4553 |
28 Aug 17 |
nicklas |
12 |
import java.util.List; |
4553 |
28 Aug 17 |
nicklas |
13 |
import java.util.Map; |
4553 |
28 Aug 17 |
nicklas |
14 |
import java.util.Set; |
4553 |
28 Aug 17 |
nicklas |
15 |
|
5510 |
18 Jun 19 |
nicklas |
16 |
import org.apache.poi.ss.usermodel.Workbook; |
5510 |
18 Jun 19 |
nicklas |
17 |
import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
5510 |
18 Jun 19 |
nicklas |
18 |
import org.apache.poi.xssf.usermodel.XSSFWorkbookType; |
5510 |
18 Jun 19 |
nicklas |
19 |
|
4553 |
28 Aug 17 |
nicklas |
20 |
import net.sf.basedb.core.Annotatable; |
4553 |
28 Aug 17 |
nicklas |
21 |
import net.sf.basedb.core.AnnotationType; |
4553 |
28 Aug 17 |
nicklas |
22 |
import net.sf.basedb.core.BaseException; |
4553 |
28 Aug 17 |
nicklas |
23 |
import net.sf.basedb.core.BooleanParameterType; |
4553 |
28 Aug 17 |
nicklas |
24 |
import net.sf.basedb.core.DbControl; |
4553 |
28 Aug 17 |
nicklas |
25 |
import net.sf.basedb.core.Directory; |
4553 |
28 Aug 17 |
nicklas |
26 |
import net.sf.basedb.core.File; |
4553 |
28 Aug 17 |
nicklas |
27 |
import net.sf.basedb.core.Item; |
4553 |
28 Aug 17 |
nicklas |
28 |
import net.sf.basedb.core.ItemAlreadyExistsException; |
4553 |
28 Aug 17 |
nicklas |
29 |
import net.sf.basedb.core.ItemList; |
4553 |
28 Aug 17 |
nicklas |
30 |
import net.sf.basedb.core.ItemParameterType; |
4553 |
28 Aug 17 |
nicklas |
31 |
import net.sf.basedb.core.ItemQuery; |
4553 |
28 Aug 17 |
nicklas |
32 |
import net.sf.basedb.core.Path; |
4553 |
28 Aug 17 |
nicklas |
33 |
import net.sf.basedb.core.PathParameterType; |
4554 |
29 Aug 17 |
nicklas |
34 |
import net.sf.basedb.core.PermissionDeniedException; |
4553 |
28 Aug 17 |
nicklas |
35 |
import net.sf.basedb.core.PluginParameter; |
4553 |
28 Aug 17 |
nicklas |
36 |
import net.sf.basedb.core.ProgressReporter; |
4553 |
28 Aug 17 |
nicklas |
37 |
import net.sf.basedb.core.RequestInformation; |
5508 |
18 Jun 19 |
nicklas |
38 |
import net.sf.basedb.core.StringParameterType; |
4553 |
28 Aug 17 |
nicklas |
39 |
import net.sf.basedb.core.Job.ExecutionTime; |
4553 |
28 Aug 17 |
nicklas |
40 |
import net.sf.basedb.core.Listable; |
4553 |
28 Aug 17 |
nicklas |
41 |
import net.sf.basedb.core.Nameable; |
4553 |
28 Aug 17 |
nicklas |
42 |
import net.sf.basedb.core.plugin.AbstractPlugin; |
4553 |
28 Aug 17 |
nicklas |
43 |
import net.sf.basedb.core.plugin.GuiContext; |
4553 |
28 Aug 17 |
nicklas |
44 |
import net.sf.basedb.core.plugin.InteractivePlugin; |
4553 |
28 Aug 17 |
nicklas |
45 |
import net.sf.basedb.core.plugin.Request; |
4553 |
28 Aug 17 |
nicklas |
46 |
import net.sf.basedb.core.plugin.Response; |
4553 |
28 Aug 17 |
nicklas |
47 |
import net.sf.basedb.core.query.Expressions; |
4553 |
28 Aug 17 |
nicklas |
48 |
import net.sf.basedb.core.query.Hql; |
4553 |
28 Aug 17 |
nicklas |
49 |
import net.sf.basedb.core.query.Orders; |
4553 |
28 Aug 17 |
nicklas |
50 |
import net.sf.basedb.core.query.Restrictions; |
4553 |
28 Aug 17 |
nicklas |
51 |
import net.sf.basedb.core.signal.SignalHandler; |
4553 |
28 Aug 17 |
nicklas |
52 |
import net.sf.basedb.core.signal.SignalTarget; |
4553 |
28 Aug 17 |
nicklas |
53 |
import net.sf.basedb.core.signal.ThreadSignalHandler; |
4553 |
28 Aug 17 |
nicklas |
54 |
import net.sf.basedb.core.snapshot.AnnotationSnapshot; |
4553 |
28 Aug 17 |
nicklas |
55 |
import net.sf.basedb.core.snapshot.AnnotationTypeFilter; |
4553 |
28 Aug 17 |
nicklas |
56 |
import net.sf.basedb.core.snapshot.SnapshotManager; |
4553 |
28 Aug 17 |
nicklas |
57 |
import net.sf.basedb.relax.Relax; |
5239 |
17 Jan 19 |
nicklas |
58 |
import net.sf.basedb.relax.dao.Annotationtype; |
5510 |
18 Jun 19 |
nicklas |
59 |
import net.sf.basedb.util.FileUtil; |
4553 |
28 Aug 17 |
nicklas |
60 |
import net.sf.basedb.util.Values; |
4553 |
28 Aug 17 |
nicklas |
61 |
import net.sf.basedb.util.encode.ToSpaceEncoderDecoder; |
5510 |
18 Jun 19 |
nicklas |
62 |
import net.sf.basedb.util.excel.XlsxTableWriter; |
5510 |
18 Jun 19 |
nicklas |
63 |
import net.sf.basedb.util.excel.XlsxToCsvUtil; |
4553 |
28 Aug 17 |
nicklas |
64 |
import net.sf.basedb.util.export.TableWriter; |
4553 |
28 Aug 17 |
nicklas |
65 |
|
4553 |
28 Aug 17 |
nicklas |
66 |
/** |
4553 |
28 Aug 17 |
nicklas |
Plug-in for generating cohort data files. Input us an item list which |
4553 |
28 Aug 17 |
nicklas |
must contain items of the same subtype. Additionally, the parent chain |
4553 |
28 Aug 17 |
nicklas |
of every item must match all other items. One file is produced for each |
4553 |
28 Aug 17 |
nicklas |
step in the chain from the root item up to the topmost parent. The file |
4553 |
28 Aug 17 |
nicklas |
is named automatically after the subtype of the items in each step. |
4553 |
28 Aug 17 |
nicklas |
72 |
|
4553 |
28 Aug 17 |
nicklas |
Each file will export: |
4553 |
28 Aug 17 |
nicklas |
* Name of root item (except in the starting file) |
4553 |
28 Aug 17 |
nicklas |
* Name of current item |
4553 |
28 Aug 17 |
nicklas |
* All annotation values for the current item |
4553 |
28 Aug 17 |
nicklas |
77 |
|
4553 |
28 Aug 17 |
nicklas |
@author nicklas |
4553 |
28 Aug 17 |
nicklas |
@since 1.1 |
4553 |
28 Aug 17 |
nicklas |
80 |
*/ |
4553 |
28 Aug 17 |
nicklas |
81 |
public class CohortExporterPlugin |
4553 |
28 Aug 17 |
nicklas |
82 |
extends AbstractPlugin |
4553 |
28 Aug 17 |
nicklas |
83 |
implements InteractivePlugin, SignalTarget |
4553 |
28 Aug 17 |
nicklas |
84 |
{ |
4553 |
28 Aug 17 |
nicklas |
85 |
|
4553 |
28 Aug 17 |
nicklas |
86 |
private Set<Item> itemTypes = new HashSet<>(Arrays.asList( |
4553 |
28 Aug 17 |
nicklas |
87 |
Item.RAWBIOASSAY, Item.DERIVEDBIOASSAY, Item.EXTRACT, Item.SAMPLE, Item.BIOSOURCE)); |
4553 |
28 Aug 17 |
nicklas |
88 |
|
4553 |
28 Aug 17 |
nicklas |
89 |
private RequestInformation configureExport; |
4553 |
28 Aug 17 |
nicklas |
90 |
|
4553 |
28 Aug 17 |
nicklas |
91 |
private ThreadSignalHandler signalHandler; |
4553 |
28 Aug 17 |
nicklas |
92 |
|
4553 |
28 Aug 17 |
nicklas |
93 |
public CohortExporterPlugin() |
4553 |
28 Aug 17 |
nicklas |
94 |
{} |
4553 |
28 Aug 17 |
nicklas |
95 |
|
4553 |
28 Aug 17 |
nicklas |
96 |
@Override |
4553 |
28 Aug 17 |
nicklas |
97 |
public boolean requiresConfiguration() |
4553 |
28 Aug 17 |
nicklas |
98 |
{ |
4553 |
28 Aug 17 |
nicklas |
99 |
return false; |
4553 |
28 Aug 17 |
nicklas |
100 |
} |
4553 |
28 Aug 17 |
nicklas |
101 |
|
4553 |
28 Aug 17 |
nicklas |
102 |
/* |
4553 |
28 Aug 17 |
nicklas |
From the Plugin interface |
4553 |
28 Aug 17 |
nicklas |
104 |
-------------------------------- |
4553 |
28 Aug 17 |
nicklas |
105 |
*/ |
4553 |
28 Aug 17 |
nicklas |
106 |
@Override |
4553 |
28 Aug 17 |
nicklas |
107 |
public boolean supportsConfigurations() |
4553 |
28 Aug 17 |
nicklas |
108 |
{ |
4553 |
28 Aug 17 |
nicklas |
109 |
return false; |
4553 |
28 Aug 17 |
nicklas |
110 |
} |
4553 |
28 Aug 17 |
nicklas |
111 |
@Override |
4553 |
28 Aug 17 |
nicklas |
112 |
public MainType getMainType() |
4553 |
28 Aug 17 |
nicklas |
113 |
{ |
4553 |
28 Aug 17 |
nicklas |
114 |
return MainType.EXPORT; |
4553 |
28 Aug 17 |
nicklas |
115 |
} |
4553 |
28 Aug 17 |
nicklas |
116 |
|
4553 |
28 Aug 17 |
nicklas |
117 |
@Override |
4553 |
28 Aug 17 |
nicklas |
118 |
public void run(Request request, Response response, ProgressReporter progress) |
4553 |
28 Aug 17 |
nicklas |
119 |
{ |
4553 |
28 Aug 17 |
nicklas |
120 |
if (signalHandler != null) signalHandler.setWorkerThread(null); |
4553 |
28 Aug 17 |
nicklas |
121 |
|
4553 |
28 Aug 17 |
nicklas |
122 |
DbControl dc = null; |
4553 |
28 Aug 17 |
nicklas |
123 |
DbControl readerDc = null; |
4553 |
28 Aug 17 |
nicklas |
124 |
try |
4553 |
28 Aug 17 |
nicklas |
125 |
{ |
4553 |
28 Aug 17 |
nicklas |
126 |
dc = sc.newDbControl(); |
4553 |
28 Aug 17 |
nicklas |
127 |
|
5508 |
18 Jun 19 |
nicklas |
128 |
String fileFormat = (String)job.getValue("fileFormat"); |
4553 |
28 Aug 17 |
nicklas |
129 |
String saveToPath = (String)job.getValue("saveTo"); |
4553 |
28 Aug 17 |
nicklas |
130 |
boolean overwrite = Boolean.TRUE.equals(job.getValue("overwrite")); |
5557 |
13 Aug 19 |
nicklas |
131 |
boolean debugMode = Boolean.TRUE.equals(job.getValue("debugMode")); |
5508 |
18 Jun 19 |
nicklas |
132 |
|
5510 |
18 Jun 19 |
nicklas |
133 |
TableWriterFactory factory = null; |
5508 |
18 Jun 19 |
nicklas |
134 |
if ("XLSX".equals(fileFormat)) |
5508 |
18 Jun 19 |
nicklas |
135 |
{ |
5508 |
18 Jun 19 |
nicklas |
136 |
Path p = new Path(saveToPath, Path.Type.FILE); |
5510 |
18 Jun 19 |
nicklas |
137 |
Directory saveTo = Directory.getNew(dc, p); |
5510 |
18 Jun 19 |
nicklas |
138 |
File xlsx = File.getFile(dc, saveTo, p.getFilename(), true); |
5510 |
18 Jun 19 |
nicklas |
139 |
if (xlsx.isInDatabase() && !overwrite && !xlsx.isRemoved()) |
5508 |
18 Jun 19 |
nicklas |
140 |
{ |
5508 |
18 Jun 19 |
nicklas |
// Not allowed to overwrite the existing file |
5510 |
18 Jun 19 |
nicklas |
142 |
throw new ItemAlreadyExistsException("File[path="+xlsx.getPath()+"]"); |
5508 |
18 Jun 19 |
nicklas |
143 |
} |
5510 |
18 Jun 19 |
nicklas |
144 |
else if (!xlsx.isInDatabase()) |
5508 |
18 Jun 19 |
nicklas |
145 |
{ |
5510 |
18 Jun 19 |
nicklas |
146 |
dc.saveItem(xlsx); |
5508 |
18 Jun 19 |
nicklas |
147 |
} |
5510 |
18 Jun 19 |
nicklas |
148 |
xlsx.setMimeType(XlsxToCsvUtil.XLSX_MIME_TYPE); |
5510 |
18 Jun 19 |
nicklas |
149 |
factory = new XlsxTableWriterFactory(xlsx); |
5508 |
18 Jun 19 |
nicklas |
150 |
} |
5508 |
18 Jun 19 |
nicklas |
151 |
else |
5508 |
18 Jun 19 |
nicklas |
152 |
{ |
5508 |
18 Jun 19 |
nicklas |
// CSV |
5510 |
18 Jun 19 |
nicklas |
154 |
Directory saveTo = Directory.getNew(dc, new Path(saveToPath, Path.Type.DIRECTORY)); |
5510 |
18 Jun 19 |
nicklas |
155 |
factory = new CsvTableWriterFactory(saveTo, overwrite); |
5508 |
18 Jun 19 |
nicklas |
156 |
} |
5238 |
17 Jan 19 |
nicklas |
157 |
boolean exportChildren = Boolean.TRUE.equals(job.getValue("exportChildren")); |
4553 |
28 Aug 17 |
nicklas |
158 |
|
4553 |
28 Aug 17 |
nicklas |
// Load the list... |
4553 |
28 Aug 17 |
nicklas |
160 |
ItemList list = (ItemList)job.getValue("itemList"); |
4553 |
28 Aug 17 |
nicklas |
161 |
list = ItemList.getById(dc, list.getId()); |
4553 |
28 Aug 17 |
nicklas |
162 |
|
4553 |
28 Aug 17 |
nicklas |
// ...and all members (id only) |
4553 |
28 Aug 17 |
nicklas |
164 |
ItemQuery<? extends Listable> query = list.getMembers(); |
4553 |
28 Aug 17 |
nicklas |
165 |
query.setIncludes(Relax.INCLUDE_IN_CURRENT_PROJECT); |
4553 |
28 Aug 17 |
nicklas |
166 |
query.order(Orders.asc(Hql.property("name"))); |
4553 |
28 Aug 17 |
nicklas |
167 |
List<Integer> itemIds = query.idList(dc); |
4553 |
28 Aug 17 |
nicklas |
168 |
|
4553 |
28 Aug 17 |
nicklas |
169 |
Item memberType = list.getMemberType(); |
4553 |
28 Aug 17 |
nicklas |
170 |
Map<String, CohortTableWriter> writers = new HashMap<>(); |
4553 |
28 Aug 17 |
nicklas |
171 |
SnapshotManager manager = null; |
4553 |
28 Aug 17 |
nicklas |
172 |
int count = 0; |
4553 |
28 Aug 17 |
nicklas |
173 |
int totalCount = list.getSize(); |
5222 |
11 Jan 19 |
nicklas |
174 |
Set<String> exportedItems = new HashSet<>(); |
4553 |
28 Aug 17 |
nicklas |
175 |
for (int itemId : itemIds) |
4553 |
28 Aug 17 |
nicklas |
176 |
{ |
4553 |
28 Aug 17 |
nicklas |
177 |
if (count % 100 == 0) |
4553 |
28 Aug 17 |
nicklas |
178 |
{ |
4553 |
28 Aug 17 |
nicklas |
// Rollback and create new DbControl and Snapshot |
4553 |
28 Aug 17 |
nicklas |
// Manager to allow GC to reclaim memory |
4553 |
28 Aug 17 |
nicklas |
181 |
if (readerDc != null) readerDc.close(); |
4553 |
28 Aug 17 |
nicklas |
182 |
readerDc = dc.getSessionControl().newDbControl(); |
4553 |
28 Aug 17 |
nicklas |
183 |
manager = new SnapshotManager(); |
4553 |
28 Aug 17 |
nicklas |
184 |
} |
4553 |
28 Aug 17 |
nicklas |
185 |
if (count % 10 == 0) |
4553 |
28 Aug 17 |
nicklas |
186 |
{ |
4553 |
28 Aug 17 |
nicklas |
187 |
ThreadSignalHandler.checkInterrupted(); |
4553 |
28 Aug 17 |
nicklas |
188 |
if (progress != null) |
4553 |
28 Aug 17 |
nicklas |
189 |
{ |
4553 |
28 Aug 17 |
nicklas |
190 |
progress.display(5 + (int)((90*count)/totalCount), |
4553 |
28 Aug 17 |
nicklas |
191 |
"Exporting cohort data (" + count + " of " + totalCount + ")..."); |
4553 |
28 Aug 17 |
nicklas |
192 |
} |
4553 |
28 Aug 17 |
nicklas |
193 |
} |
4553 |
28 Aug 17 |
nicklas |
194 |
count++; |
4553 |
28 Aug 17 |
nicklas |
195 |
|
4553 |
28 Aug 17 |
nicklas |
196 |
CohortItem rootItem = new CohortItem((Nameable)memberType.getById(readerDc, itemId)); |
4553 |
28 Aug 17 |
nicklas |
197 |
CohortChain cohort = new CohortChain(readerDc, rootItem); |
4553 |
28 Aug 17 |
nicklas |
198 |
|
4553 |
28 Aug 17 |
nicklas |
199 |
List<CohortItem> parents = cohort.getParentItems(); |
4553 |
28 Aug 17 |
nicklas |
200 |
for (CohortItem parent : parents) |
4553 |
28 Aug 17 |
nicklas |
201 |
{ |
5222 |
11 Jan 19 |
nicklas |
202 |
if (!exportedItems.add(parent.getName())) continue; // Skipping duplicate |
5222 |
11 Jan 19 |
nicklas |
203 |
|
4553 |
28 Aug 17 |
nicklas |
204 |
String subtype = parent.getSubtype(); |
4553 |
28 Aug 17 |
nicklas |
205 |
CohortTableWriter writer = writers.get(subtype); |
5222 |
11 Jan 19 |
nicklas |
206 |
if (writer == null) |
5222 |
11 Jan 19 |
nicklas |
207 |
{ |
5510 |
18 Jun 19 |
nicklas |
208 |
writer = factory.createCohortWriter(dc, cohort, parent); |
5222 |
11 Jan 19 |
nicklas |
209 |
writers.put(subtype, writer); |
5222 |
11 Jan 19 |
nicklas |
210 |
} |
5227 |
14 Jan 19 |
nicklas |
211 |
writer.writeItem(parent, cohort, readerDc, manager); |
4553 |
28 Aug 17 |
nicklas |
212 |
} |
5219 |
11 Jan 19 |
nicklas |
213 |
|
5238 |
17 Jan 19 |
nicklas |
214 |
if (exportChildren) |
5219 |
11 Jan 19 |
nicklas |
215 |
{ |
5238 |
17 Jan 19 |
nicklas |
216 |
List<CohortItem> children = cohort.getChildItems(); |
5238 |
17 Jan 19 |
nicklas |
217 |
for (CohortItem child : children) |
5219 |
11 Jan 19 |
nicklas |
218 |
{ |
5238 |
17 Jan 19 |
nicklas |
219 |
if (!exportedItems.add(child.getName())) continue; |
5238 |
17 Jan 19 |
nicklas |
220 |
|
5238 |
17 Jan 19 |
nicklas |
221 |
String subtype = child.getSubtype(); |
5238 |
17 Jan 19 |
nicklas |
222 |
CohortTableWriter writer = writers.get(subtype); |
5238 |
17 Jan 19 |
nicklas |
223 |
if (writer == null) |
5238 |
17 Jan 19 |
nicklas |
224 |
{ |
5510 |
18 Jun 19 |
nicklas |
225 |
writer = factory.createCohortWriter(dc, cohort, child); |
5238 |
17 Jan 19 |
nicklas |
226 |
writers.put(subtype, writer); |
5238 |
17 Jan 19 |
nicklas |
227 |
} |
5238 |
17 Jan 19 |
nicklas |
228 |
writer.writeItem(child, cohort, readerDc, manager); |
5219 |
11 Jan 19 |
nicklas |
229 |
} |
5219 |
11 Jan 19 |
nicklas |
230 |
} |
4553 |
28 Aug 17 |
nicklas |
231 |
} |
4553 |
28 Aug 17 |
nicklas |
232 |
|
4553 |
28 Aug 17 |
nicklas |
233 |
for (CohortTableWriter writer : writers.values()) |
4553 |
28 Aug 17 |
nicklas |
234 |
{ |
4553 |
28 Aug 17 |
nicklas |
235 |
writer.close(); |
4553 |
28 Aug 17 |
nicklas |
236 |
} |
5510 |
18 Jun 19 |
nicklas |
237 |
factory.close(); |
4553 |
28 Aug 17 |
nicklas |
238 |
|
4553 |
28 Aug 17 |
nicklas |
239 |
dc.commit(); |
4553 |
28 Aug 17 |
nicklas |
240 |
String msg = totalCount + " items in the '" + list.getName() + "' list have been exported to " + saveToPath; |
5557 |
13 Aug 19 |
nicklas |
241 |
if (debugMode) |
5557 |
13 Aug 19 |
nicklas |
242 |
{ |
5557 |
13 Aug 19 |
nicklas |
// DEBUG!! will make it easier to re-run a job with same settings |
5557 |
13 Aug 19 |
nicklas |
244 |
response.setError("[DEBUG] "+msg, null); |
5557 |
13 Aug 19 |
nicklas |
245 |
} |
5557 |
13 Aug 19 |
nicklas |
246 |
else |
5557 |
13 Aug 19 |
nicklas |
247 |
{ |
5557 |
13 Aug 19 |
nicklas |
248 |
response.setDone(msg); |
5557 |
13 Aug 19 |
nicklas |
249 |
} |
4553 |
28 Aug 17 |
nicklas |
250 |
} |
4553 |
28 Aug 17 |
nicklas |
251 |
catch (Throwable t) |
4553 |
28 Aug 17 |
nicklas |
252 |
{ |
4553 |
28 Aug 17 |
nicklas |
253 |
response.setError(t.getMessage(), Arrays.asList(t)); |
4553 |
28 Aug 17 |
nicklas |
254 |
} |
4553 |
28 Aug 17 |
nicklas |
255 |
finally |
4553 |
28 Aug 17 |
nicklas |
256 |
{ |
5510 |
18 Jun 19 |
nicklas |
257 |
|
4553 |
28 Aug 17 |
nicklas |
258 |
if (readerDc != null) readerDc.close(); |
4553 |
28 Aug 17 |
nicklas |
259 |
if (dc != null) dc.close(); |
4553 |
28 Aug 17 |
nicklas |
260 |
} |
4553 |
28 Aug 17 |
nicklas |
261 |
|
4553 |
28 Aug 17 |
nicklas |
262 |
} |
4553 |
28 Aug 17 |
nicklas |
263 |
// ------------------------------------- |
4553 |
28 Aug 17 |
nicklas |
264 |
/* |
4553 |
28 Aug 17 |
nicklas |
From the InteractivePlugin interface |
4553 |
28 Aug 17 |
nicklas |
266 |
------------------------------------------- |
4553 |
28 Aug 17 |
nicklas |
267 |
*/ |
4553 |
28 Aug 17 |
nicklas |
268 |
/** |
4553 |
28 Aug 17 |
nicklas |
The plug-in will appear on the single-item view |
4553 |
28 Aug 17 |
nicklas |
for item lists. |
4553 |
28 Aug 17 |
nicklas |
271 |
*/ |
4553 |
28 Aug 17 |
nicklas |
272 |
@Override |
4553 |
28 Aug 17 |
nicklas |
273 |
public Set<GuiContext> getGuiContexts() |
4553 |
28 Aug 17 |
nicklas |
274 |
{ |
4553 |
28 Aug 17 |
nicklas |
275 |
return Collections.singleton(GuiContext.item(Item.ITEMLIST)); |
4553 |
28 Aug 17 |
nicklas |
276 |
} |
4553 |
28 Aug 17 |
nicklas |
277 |
/** |
4553 |
28 Aug 17 |
nicklas |
We accept all item lists with: |
4553 |
28 Aug 17 |
nicklas |
* Raw bioassays |
4553 |
28 Aug 17 |
nicklas |
* Derived bioassays |
4553 |
28 Aug 17 |
nicklas |
* Extracts |
4553 |
28 Aug 17 |
nicklas |
* Samples |
4553 |
28 Aug 17 |
nicklas |
* Biosources |
4553 |
28 Aug 17 |
nicklas |
The list must have at least one member. |
4553 |
28 Aug 17 |
nicklas |
285 |
*/ |
4553 |
28 Aug 17 |
nicklas |
286 |
@Override |
4553 |
28 Aug 17 |
nicklas |
287 |
public String isInContext(GuiContext context, Object item) |
4553 |
28 Aug 17 |
nicklas |
288 |
{ |
4553 |
28 Aug 17 |
nicklas |
289 |
String message = null; |
4553 |
28 Aug 17 |
nicklas |
290 |
if (item == null) |
4553 |
28 Aug 17 |
nicklas |
291 |
{ |
4553 |
28 Aug 17 |
nicklas |
292 |
message = "The object is null"; |
4553 |
28 Aug 17 |
nicklas |
293 |
} |
4553 |
28 Aug 17 |
nicklas |
294 |
else if (!(item instanceof ItemList)) |
4553 |
28 Aug 17 |
nicklas |
295 |
{ |
4553 |
28 Aug 17 |
nicklas |
296 |
message = "The object is not an item list: " + item; |
4553 |
28 Aug 17 |
nicklas |
297 |
} |
4553 |
28 Aug 17 |
nicklas |
298 |
ItemList list = (ItemList)item; |
4553 |
28 Aug 17 |
nicklas |
299 |
if (!itemTypes.contains(list.getMemberType())) |
4553 |
28 Aug 17 |
nicklas |
300 |
{ |
4554 |
29 Aug 17 |
nicklas |
301 |
throw new BaseException("This plug-in need an item list with one of: " + Values.getString(itemTypes, ", ", true)); |
4553 |
28 Aug 17 |
nicklas |
302 |
} |
4553 |
28 Aug 17 |
nicklas |
303 |
else if (list.getSize() == 0) |
4553 |
28 Aug 17 |
nicklas |
304 |
{ |
4554 |
29 Aug 17 |
nicklas |
305 |
throw new BaseException("The selected list is empty."); |
4553 |
28 Aug 17 |
nicklas |
306 |
} |
4554 |
29 Aug 17 |
nicklas |
307 |
else if (sc.getActiveProjectId() == 0) |
4554 |
29 Aug 17 |
nicklas |
308 |
{ |
4554 |
29 Aug 17 |
nicklas |
309 |
throw new BaseException("This plug-in requires an active project!"); |
4554 |
29 Aug 17 |
nicklas |
310 |
} |
4553 |
28 Aug 17 |
nicklas |
311 |
return message; |
4553 |
28 Aug 17 |
nicklas |
312 |
} |
4553 |
28 Aug 17 |
nicklas |
313 |
@Override |
4553 |
28 Aug 17 |
nicklas |
314 |
public RequestInformation getRequestInformation(GuiContext context, String command) |
4553 |
28 Aug 17 |
nicklas |
315 |
throws BaseException |
4553 |
28 Aug 17 |
nicklas |
316 |
{ |
4553 |
28 Aug 17 |
nicklas |
317 |
RequestInformation requestInformation = null; |
4553 |
28 Aug 17 |
nicklas |
318 |
if (Request.COMMAND_CONFIGURE_JOB.equals(command)) |
4553 |
28 Aug 17 |
nicklas |
319 |
{ |
4554 |
29 Aug 17 |
nicklas |
320 |
if (sc.getActiveProjectId() == 0) |
4554 |
29 Aug 17 |
nicklas |
321 |
{ |
4554 |
29 Aug 17 |
nicklas |
322 |
throw new PermissionDeniedException("This plug-in requires an active project!"); |
4554 |
29 Aug 17 |
nicklas |
323 |
} |
4553 |
28 Aug 17 |
nicklas |
324 |
requestInformation = getConfigureExportParameters(); |
4553 |
28 Aug 17 |
nicklas |
325 |
} |
4553 |
28 Aug 17 |
nicklas |
326 |
return requestInformation; |
4553 |
28 Aug 17 |
nicklas |
327 |
} |
4553 |
28 Aug 17 |
nicklas |
328 |
|
4553 |
28 Aug 17 |
nicklas |
329 |
@Override |
4553 |
28 Aug 17 |
nicklas |
330 |
public void configure(GuiContext context, Request request, Response response) |
4553 |
28 Aug 17 |
nicklas |
331 |
{ |
4553 |
28 Aug 17 |
nicklas |
332 |
String command = request.getCommand(); |
4553 |
28 Aug 17 |
nicklas |
333 |
try |
4553 |
28 Aug 17 |
nicklas |
334 |
{ |
4553 |
28 Aug 17 |
nicklas |
335 |
if (command.equals(Request.COMMAND_CONFIGURE_JOB)) |
4553 |
28 Aug 17 |
nicklas |
336 |
{ |
4553 |
28 Aug 17 |
nicklas |
337 |
RequestInformation ri = getConfigureExportParameters(); |
4553 |
28 Aug 17 |
nicklas |
338 |
List<Throwable> errors = validateRequestParameters(ri.getParameters(), request); |
4553 |
28 Aug 17 |
nicklas |
339 |
if (errors != null) |
4553 |
28 Aug 17 |
nicklas |
340 |
{ |
4553 |
28 Aug 17 |
nicklas |
341 |
response.setError(errors.size() + " invalid parameters were found in the request", errors); |
4553 |
28 Aug 17 |
nicklas |
342 |
return; |
4553 |
28 Aug 17 |
nicklas |
343 |
} |
4553 |
28 Aug 17 |
nicklas |
344 |
|
4553 |
28 Aug 17 |
nicklas |
// We need to verify that all items in the list are of the same subtype |
4553 |
28 Aug 17 |
nicklas |
346 |
ItemList list = (ItemList)request.getParameterValue("itemList"); |
4553 |
28 Aug 17 |
nicklas |
347 |
String error = validateList(list); |
4553 |
28 Aug 17 |
nicklas |
348 |
if (error != null) |
4553 |
28 Aug 17 |
nicklas |
349 |
{ |
4553 |
28 Aug 17 |
nicklas |
350 |
response.setError(error, null); |
4553 |
28 Aug 17 |
nicklas |
351 |
return; |
4553 |
28 Aug 17 |
nicklas |
352 |
} |
4553 |
28 Aug 17 |
nicklas |
353 |
|
4553 |
28 Aug 17 |
nicklas |
354 |
String path = (String)request.getParameterValue("saveTo"); |
4553 |
28 Aug 17 |
nicklas |
355 |
storeValue(job, request, ri.getParameter("itemList")); |
5238 |
17 Jan 19 |
nicklas |
356 |
storeValue(job, request, ri.getParameter("exportChildren")); |
5508 |
18 Jun 19 |
nicklas |
357 |
storeValue(job, request, ri.getParameter("fileFormat")); |
4553 |
28 Aug 17 |
nicklas |
358 |
storeValue(job, request, ri.getParameter("saveTo")); |
4553 |
28 Aug 17 |
nicklas |
359 |
storeValue(job, request, ri.getParameter("overwrite")); |
5557 |
13 Aug 19 |
nicklas |
360 |
storeValue(job, request, ri.getParameter("debugMode")); |
4553 |
28 Aug 17 |
nicklas |
361 |
|
4553 |
28 Aug 17 |
nicklas |
362 |
response.setSuggestedJobName("Cohort export for '" + list.getName() + "' to " + path); |
4553 |
28 Aug 17 |
nicklas |
363 |
response.setDone("The job configuration is complete", ExecutionTime.MEDIUM); |
4553 |
28 Aug 17 |
nicklas |
364 |
} |
4553 |
28 Aug 17 |
nicklas |
365 |
} |
4553 |
28 Aug 17 |
nicklas |
366 |
catch (Throwable ex) |
4553 |
28 Aug 17 |
nicklas |
367 |
{ |
4553 |
28 Aug 17 |
nicklas |
368 |
response.setError(ex.getMessage(), Arrays.asList(ex)); |
4553 |
28 Aug 17 |
nicklas |
369 |
} |
4553 |
28 Aug 17 |
nicklas |
370 |
} |
4553 |
28 Aug 17 |
nicklas |
371 |
|
4553 |
28 Aug 17 |
nicklas |
372 |
/** |
4553 |
28 Aug 17 |
nicklas |
Check that all items in the list are of the same subtype. |
4553 |
28 Aug 17 |
nicklas |
This is an initial check only which should be fairly quick. |
4553 |
28 Aug 17 |
nicklas |
During the export, additional checks are made to ensure that |
4553 |
28 Aug 17 |
nicklas |
all parent items are following the same path up to the topmost |
4553 |
28 Aug 17 |
nicklas |
parent. |
4553 |
28 Aug 17 |
nicklas |
378 |
*/ |
4553 |
28 Aug 17 |
nicklas |
379 |
private String validateList(ItemList list) |
4553 |
28 Aug 17 |
nicklas |
380 |
{ |
4553 |
28 Aug 17 |
nicklas |
381 |
DbControl dc = null; |
4553 |
28 Aug 17 |
nicklas |
382 |
try |
4553 |
28 Aug 17 |
nicklas |
383 |
{ |
4553 |
28 Aug 17 |
nicklas |
384 |
dc = sc.newDbControl(); |
4553 |
28 Aug 17 |
nicklas |
385 |
ItemQuery<? extends Listable> query = list.getMembers(); |
4553 |
28 Aug 17 |
nicklas |
386 |
query.setIncludes(Relax.INCLUDE_IN_CURRENT_PROJECT); |
4553 |
28 Aug 17 |
nicklas |
387 |
query.order(Orders.asc(Hql.property("name"))); |
4553 |
28 Aug 17 |
nicklas |
388 |
|
4553 |
28 Aug 17 |
nicklas |
389 |
List<? extends Listable> items = query.list(dc); |
5246 |
18 Jan 19 |
nicklas |
390 |
if (items.size() == 0) return "The '"+list.getName()+"' list is empty."; |
4553 |
28 Aug 17 |
nicklas |
391 |
|
5246 |
18 Jan 19 |
nicklas |
392 |
if (list.getSize() != items.size()) |
5246 |
18 Jan 19 |
nicklas |
393 |
{ |
5246 |
18 Jan 19 |
nicklas |
394 |
int notInProject = list.getSize() - items.size(); |
5246 |
18 Jan 19 |
nicklas |
395 |
return "The '"+list.getName()+"' list contains "+notInProject+" items that are not shared to the current project."; |
5246 |
18 Jan 19 |
nicklas |
396 |
} |
5246 |
18 Jan 19 |
nicklas |
397 |
|
4553 |
28 Aug 17 |
nicklas |
398 |
for (Listable item : items) |
4553 |
28 Aug 17 |
nicklas |
399 |
{ |
4553 |
28 Aug 17 |
nicklas |
400 |
CohortItem ch = new CohortItem(item); |
4553 |
28 Aug 17 |
nicklas |
401 |
String subtype = ch.getSubtype(); |
4553 |
28 Aug 17 |
nicklas |
402 |
if (subtype == null) return "Missing subtype on item '" + item.getName() + "'."; |
4553 |
28 Aug 17 |
nicklas |
403 |
} |
4553 |
28 Aug 17 |
nicklas |
404 |
} |
4553 |
28 Aug 17 |
nicklas |
405 |
finally |
4553 |
28 Aug 17 |
nicklas |
406 |
{ |
4553 |
28 Aug 17 |
nicklas |
407 |
if (dc != null) dc.close(); |
4553 |
28 Aug 17 |
nicklas |
408 |
} |
4553 |
28 Aug 17 |
nicklas |
409 |
return null; |
4553 |
28 Aug 17 |
nicklas |
410 |
} |
4553 |
28 Aug 17 |
nicklas |
411 |
|
4553 |
28 Aug 17 |
nicklas |
412 |
|
4553 |
28 Aug 17 |
nicklas |
413 |
/* |
4553 |
28 Aug 17 |
nicklas |
From the SignalTarget interface |
4553 |
28 Aug 17 |
nicklas |
415 |
------------------------------------------- |
4553 |
28 Aug 17 |
nicklas |
416 |
*/ |
4553 |
28 Aug 17 |
nicklas |
417 |
@Override |
4553 |
28 Aug 17 |
nicklas |
418 |
public SignalHandler getSignalHandler() |
4553 |
28 Aug 17 |
nicklas |
419 |
{ |
4553 |
28 Aug 17 |
nicklas |
420 |
signalHandler = new ThreadSignalHandler(); |
4553 |
28 Aug 17 |
nicklas |
421 |
return signalHandler; |
4553 |
28 Aug 17 |
nicklas |
422 |
} |
4553 |
28 Aug 17 |
nicklas |
423 |
// ------------------------------------------- |
4553 |
28 Aug 17 |
nicklas |
424 |
|
4553 |
28 Aug 17 |
nicklas |
425 |
|
4553 |
28 Aug 17 |
nicklas |
426 |
private RequestInformation getConfigureExportParameters() |
4553 |
28 Aug 17 |
nicklas |
427 |
{ |
4553 |
28 Aug 17 |
nicklas |
428 |
if (configureExport == null) |
4553 |
28 Aug 17 |
nicklas |
429 |
{ |
4553 |
28 Aug 17 |
nicklas |
// Load the current item list |
4553 |
28 Aug 17 |
nicklas |
431 |
ItemList currentList = null; |
5238 |
17 Jan 19 |
nicklas |
432 |
Item listType = null; |
4553 |
28 Aug 17 |
nicklas |
433 |
int currentListId = sc.getCurrentContext(Item.ITEMLIST).getId(); |
4553 |
28 Aug 17 |
nicklas |
434 |
if (currentListId != 0) |
4553 |
28 Aug 17 |
nicklas |
435 |
{ |
4553 |
28 Aug 17 |
nicklas |
436 |
DbControl dc = sc.newDbControl(); |
4553 |
28 Aug 17 |
nicklas |
437 |
try |
4553 |
28 Aug 17 |
nicklas |
438 |
{ |
4553 |
28 Aug 17 |
nicklas |
439 |
currentList = ItemList.getById(dc, currentListId); |
5238 |
17 Jan 19 |
nicklas |
440 |
listType = currentList.getMemberType(); |
4553 |
28 Aug 17 |
nicklas |
441 |
} |
4553 |
28 Aug 17 |
nicklas |
442 |
finally |
4553 |
28 Aug 17 |
nicklas |
443 |
{ |
4553 |
28 Aug 17 |
nicklas |
444 |
if (dc != null) dc.close(); |
4553 |
28 Aug 17 |
nicklas |
445 |
} |
4553 |
28 Aug 17 |
nicklas |
446 |
} |
4553 |
28 Aug 17 |
nicklas |
447 |
|
4553 |
28 Aug 17 |
nicklas |
448 |
List<PluginParameter<?>> parameters = new ArrayList<PluginParameter<?>>(); |
4553 |
28 Aug 17 |
nicklas |
449 |
|
4553 |
28 Aug 17 |
nicklas |
450 |
parameters.add(new PluginParameter<ItemList>( |
4553 |
28 Aug 17 |
nicklas |
451 |
"itemList", "Item list", "Select the list with items to export. "+ |
5238 |
17 Jan 19 |
nicklas |
452 |
"Parent items are always included in the export.", |
4553 |
28 Aug 17 |
nicklas |
453 |
new ItemParameterType<ItemList>(ItemList.class, currentList, true, 1, null) |
4553 |
28 Aug 17 |
nicklas |
454 |
)); |
4553 |
28 Aug 17 |
nicklas |
455 |
|
5238 |
17 Jan 19 |
nicklas |
456 |
if (listType != Item.RAWBIOASSAY) |
5238 |
17 Jan 19 |
nicklas |
457 |
{ |
5238 |
17 Jan 19 |
nicklas |
458 |
parameters.add(new PluginParameter<Boolean>( |
5238 |
17 Jan 19 |
nicklas |
459 |
"exportChildren", "Export child items", "If this option is selected child items are also exported.", |
5238 |
17 Jan 19 |
nicklas |
460 |
new BooleanParameterType(listType == Item.BIOSOURCE ? true : null, false) |
5238 |
17 Jan 19 |
nicklas |
461 |
)); |
5238 |
17 Jan 19 |
nicklas |
462 |
} |
5238 |
17 Jan 19 |
nicklas |
463 |
|
4553 |
28 Aug 17 |
nicklas |
464 |
parameters.add(new PluginParameter<String>( |
5508 |
18 Jun 19 |
nicklas |
465 |
"fileFormat", "File format", "Select which file format to export to. If CSV is selected, " + |
5508 |
18 Jun 19 |
nicklas |
466 |
"the data is saved to multiple CSV files in the 'Save to' directory. If XLSX is selected, " + |
5508 |
18 Jun 19 |
nicklas |
467 |
"the data is saved to multiple worksheets in a single Excel file.", |
5508 |
18 Jun 19 |
nicklas |
468 |
new StringParameterType(255, "CSV", true, 1, 0, 0, Arrays.asList("CSV", "XLSX")) |
5508 |
18 Jun 19 |
nicklas |
469 |
)); |
5508 |
18 Jun 19 |
nicklas |
470 |
|
5508 |
18 Jun 19 |
nicklas |
471 |
parameters.add(new PluginParameter<String>( |
4553 |
28 Aug 17 |
nicklas |
472 |
"saveTo", "Save to", "Select the location where files should be saved.", |
5508 |
18 Jun 19 |
nicklas |
473 |
new PathParameterType(Path.Type.FILE, null, true) |
4553 |
28 Aug 17 |
nicklas |
474 |
)); |
4553 |
28 Aug 17 |
nicklas |
475 |
|
4553 |
28 Aug 17 |
nicklas |
476 |
parameters.add(new PluginParameter<Boolean>( |
4553 |
28 Aug 17 |
nicklas |
477 |
"overwrite", "Overwrite", "Is it allowed to overwrite existing files or not?", |
4553 |
28 Aug 17 |
nicklas |
478 |
new BooleanParameterType(null, false) |
4553 |
28 Aug 17 |
nicklas |
479 |
)); |
5557 |
13 Aug 19 |
nicklas |
480 |
|
5557 |
13 Aug 19 |
nicklas |
// If <developer-mode>1</developer-mode> is in relax-config.xml we allow debug via parameter |
5557 |
13 Aug 19 |
nicklas |
482 |
if (Values.getBoolean(Relax.getConfig().getConfig("developer-mode"))) |
5557 |
13 Aug 19 |
nicklas |
483 |
{ |
5557 |
13 Aug 19 |
nicklas |
484 |
parameters.add(new PluginParameter<Boolean>( |
5557 |
13 Aug 19 |
nicklas |
485 |
"debugMode", "Debug mode", "Runs the plug-in in debug mode. Major difference is " |
5557 |
13 Aug 19 |
nicklas |
486 |
+ "that it will always report an error making it easier to re-start the " |
5557 |
13 Aug 19 |
nicklas |
487 |
+ "plug-in with the same parameters.", |
5557 |
13 Aug 19 |
nicklas |
488 |
new BooleanParameterType(null, false) |
5557 |
13 Aug 19 |
nicklas |
489 |
)); |
5557 |
13 Aug 19 |
nicklas |
490 |
} |
5557 |
13 Aug 19 |
nicklas |
491 |
|
4553 |
28 Aug 17 |
nicklas |
492 |
configureExport = new RequestInformation |
4553 |
28 Aug 17 |
nicklas |
493 |
( |
4553 |
28 Aug 17 |
nicklas |
494 |
Request.COMMAND_CONFIGURE_JOB, |
4553 |
28 Aug 17 |
nicklas |
495 |
"Cohort exporter options", |
4553 |
28 Aug 17 |
nicklas |
496 |
"Select the item list and where to save the exported data files.", |
4553 |
28 Aug 17 |
nicklas |
497 |
parameters |
4553 |
28 Aug 17 |
nicklas |
498 |
); |
4553 |
28 Aug 17 |
nicklas |
499 |
} |
4553 |
28 Aug 17 |
nicklas |
500 |
return configureExport; |
4553 |
28 Aug 17 |
nicklas |
501 |
} |
4553 |
28 Aug 17 |
nicklas |
502 |
|
5510 |
18 Jun 19 |
nicklas |
503 |
/** |
5510 |
18 Jun 19 |
nicklas |
Factories are used to create CohortTableWriter and the underlying TableWriter. Typically |
5510 |
18 Jun 19 |
nicklas |
we have one writer per item subtype. |
5510 |
18 Jun 19 |
nicklas |
506 |
*/ |
5510 |
18 Jun 19 |
nicklas |
507 |
static abstract class TableWriterFactory |
5510 |
18 Jun 19 |
nicklas |
508 |
implements AutoCloseable |
4553 |
28 Aug 17 |
nicklas |
509 |
{ |
5227 |
14 Jan 19 |
nicklas |
510 |
|
5510 |
18 Jun 19 |
nicklas |
511 |
CohortTableWriter createCohortWriter(DbControl dc, CohortChain chain, CohortItem item) |
5510 |
18 Jun 19 |
nicklas |
512 |
{ |
5510 |
18 Jun 19 |
nicklas |
513 |
String subtype = item.getSubtype(); |
5510 |
18 Jun 19 |
nicklas |
514 |
TableWriter out = createTableWriter(dc, subtype); |
5510 |
18 Jun 19 |
nicklas |
515 |
|
5510 |
18 Jun 19 |
nicklas |
516 |
Nameable parent = item.getParentItem(dc); |
5510 |
18 Jun 19 |
nicklas |
517 |
String parentSubtype = parent != null ? new CohortItem(parent).getSubtype() : null; |
5510 |
18 Jun 19 |
nicklas |
518 |
CohortItem topItem = chain.getTopItem(); |
5510 |
18 Jun 19 |
nicklas |
519 |
String topSubtype = topItem != null ? topItem.getSubtype() : null; |
5510 |
18 Jun 19 |
nicklas |
520 |
|
5510 |
18 Jun 19 |
nicklas |
521 |
CohortTableWriter writer = new CohortTableWriter(dc, item.getType(), subtype, parentSubtype, topSubtype, out); |
5510 |
18 Jun 19 |
nicklas |
522 |
return writer; |
4553 |
28 Aug 17 |
nicklas |
523 |
} |
5510 |
18 Jun 19 |
nicklas |
524 |
|
5510 |
18 Jun 19 |
nicklas |
525 |
abstract TableWriter createTableWriter(DbControl dc, String subtype); |
5510 |
18 Jun 19 |
nicklas |
526 |
} |
5510 |
18 Jun 19 |
nicklas |
527 |
|
5510 |
18 Jun 19 |
nicklas |
528 |
/** |
5510 |
18 Jun 19 |
nicklas |
This writer factory directs written data to CSV files. One file is created per |
5510 |
18 Jun 19 |
nicklas |
item subtype in a given directory. |
5510 |
18 Jun 19 |
nicklas |
531 |
*/ |
5510 |
18 Jun 19 |
nicklas |
532 |
static class CsvTableWriterFactory |
5510 |
18 Jun 19 |
nicklas |
533 |
extends TableWriterFactory |
5510 |
18 Jun 19 |
nicklas |
534 |
{ |
5510 |
18 Jun 19 |
nicklas |
535 |
|
5510 |
18 Jun 19 |
nicklas |
536 |
private final Directory saveTo; |
5510 |
18 Jun 19 |
nicklas |
537 |
private final boolean overwrite; |
5510 |
18 Jun 19 |
nicklas |
538 |
|
5510 |
18 Jun 19 |
nicklas |
539 |
public CsvTableWriterFactory(Directory saveTo, boolean overwrite) |
4553 |
28 Aug 17 |
nicklas |
540 |
{ |
5510 |
18 Jun 19 |
nicklas |
541 |
this.saveTo = saveTo; |
5510 |
18 Jun 19 |
nicklas |
542 |
this.overwrite = overwrite; |
4553 |
28 Aug 17 |
nicklas |
543 |
} |
5510 |
18 Jun 19 |
nicklas |
544 |
|
5510 |
18 Jun 19 |
nicklas |
545 |
@Override |
5510 |
18 Jun 19 |
nicklas |
546 |
TableWriter createTableWriter(DbControl dc, String subtype) |
5510 |
18 Jun 19 |
nicklas |
547 |
{ |
5510 |
18 Jun 19 |
nicklas |
548 |
File file = File.getFile(dc, saveTo, subtype.toLowerCase()+".txt", true); |
5510 |
18 Jun 19 |
nicklas |
549 |
if (file.isInDatabase() && !overwrite && !file.isRemoved()) |
5510 |
18 Jun 19 |
nicklas |
550 |
{ |
5510 |
18 Jun 19 |
nicklas |
// Not allowed to overwrite the existing file |
5510 |
18 Jun 19 |
nicklas |
552 |
throw new ItemAlreadyExistsException("File[path="+file.getPath()+"]"); |
5510 |
18 Jun 19 |
nicklas |
553 |
} |
5510 |
18 Jun 19 |
nicklas |
554 |
else if (!file.isInDatabase()) |
5510 |
18 Jun 19 |
nicklas |
555 |
{ |
5510 |
18 Jun 19 |
nicklas |
556 |
dc.saveItem(file); |
5510 |
18 Jun 19 |
nicklas |
557 |
} |
5510 |
18 Jun 19 |
nicklas |
558 |
file.setCharacterSet("UTF-8"); |
5510 |
18 Jun 19 |
nicklas |
559 |
file.setMimeType("text/plain"); |
5510 |
18 Jun 19 |
nicklas |
560 |
return new TableWriter(new OutputStreamWriter(file.getUploadStream(false), StandardCharsets.UTF_8)); |
5510 |
18 Jun 19 |
nicklas |
561 |
} |
4553 |
28 Aug 17 |
nicklas |
562 |
|
5510 |
18 Jun 19 |
nicklas |
// Nothing to do |
5510 |
18 Jun 19 |
nicklas |
564 |
@Override |
5510 |
18 Jun 19 |
nicklas |
565 |
public void close() |
5510 |
18 Jun 19 |
nicklas |
566 |
{} |
4553 |
28 Aug 17 |
nicklas |
567 |
} |
4553 |
28 Aug 17 |
nicklas |
568 |
|
5510 |
18 Jun 19 |
nicklas |
569 |
/** |
5510 |
18 Jun 19 |
nicklas |
This writer factory directs written data to worksheets in an Excel file. |
5510 |
18 Jun 19 |
nicklas |
One worksheet is created per item subtype. |
5510 |
18 Jun 19 |
nicklas |
572 |
*/ |
5510 |
18 Jun 19 |
nicklas |
573 |
static class XlsxTableWriterFactory |
5510 |
18 Jun 19 |
nicklas |
574 |
extends TableWriterFactory |
5510 |
18 Jun 19 |
nicklas |
575 |
{ |
5510 |
18 Jun 19 |
nicklas |
576 |
|
5510 |
18 Jun 19 |
nicklas |
577 |
private final Workbook workbook; |
5510 |
18 Jun 19 |
nicklas |
578 |
private final File xlsx; |
5510 |
18 Jun 19 |
nicklas |
579 |
|
5510 |
18 Jun 19 |
nicklas |
580 |
XlsxTableWriterFactory(File xlsx) |
5510 |
18 Jun 19 |
nicklas |
581 |
{ |
5510 |
18 Jun 19 |
nicklas |
582 |
this.xlsx = xlsx; |
5510 |
18 Jun 19 |
nicklas |
583 |
workbook = new XSSFWorkbook(XSSFWorkbookType.XLSX); |
5510 |
18 Jun 19 |
nicklas |
584 |
} |
5510 |
18 Jun 19 |
nicklas |
585 |
|
5510 |
18 Jun 19 |
nicklas |
586 |
@Override |
5510 |
18 Jun 19 |
nicklas |
587 |
TableWriter createTableWriter(DbControl dc, String subtype) |
5510 |
18 Jun 19 |
nicklas |
588 |
{ |
5510 |
18 Jun 19 |
nicklas |
589 |
return new XlsxTableWriter(workbook, subtype); |
5510 |
18 Jun 19 |
nicklas |
590 |
} |
5510 |
18 Jun 19 |
nicklas |
591 |
|
5510 |
18 Jun 19 |
nicklas |
592 |
/** |
5510 |
18 Jun 19 |
nicklas |
Write the workbook to the file that was given at construction. |
5510 |
18 Jun 19 |
nicklas |
594 |
*/ |
5510 |
18 Jun 19 |
nicklas |
595 |
@Override |
5510 |
18 Jun 19 |
nicklas |
596 |
public void close() |
5510 |
18 Jun 19 |
nicklas |
597 |
throws IOException |
5510 |
18 Jun 19 |
nicklas |
598 |
{ |
5510 |
18 Jun 19 |
nicklas |
599 |
OutputStream out = null; |
5510 |
18 Jun 19 |
nicklas |
600 |
try |
5510 |
18 Jun 19 |
nicklas |
601 |
{ |
5510 |
18 Jun 19 |
nicklas |
602 |
out = xlsx.getUploadStream(false); |
5510 |
18 Jun 19 |
nicklas |
603 |
workbook.write(out); |
5510 |
18 Jun 19 |
nicklas |
604 |
} |
5510 |
18 Jun 19 |
nicklas |
605 |
finally |
5510 |
18 Jun 19 |
nicklas |
606 |
{ |
5510 |
18 Jun 19 |
nicklas |
607 |
FileUtil.close(out); |
5510 |
18 Jun 19 |
nicklas |
608 |
FileUtil.close(workbook); |
5510 |
18 Jun 19 |
nicklas |
609 |
} |
5510 |
18 Jun 19 |
nicklas |
610 |
} |
5510 |
18 Jun 19 |
nicklas |
611 |
} |
5510 |
18 Jun 19 |
nicklas |
612 |
|
4553 |
28 Aug 17 |
nicklas |
613 |
static class CohortTableWriter |
5510 |
18 Jun 19 |
nicklas |
614 |
implements AutoCloseable |
4553 |
28 Aug 17 |
nicklas |
615 |
{ |
5510 |
18 Jun 19 |
nicklas |
616 |
private final TableWriter out; |
4553 |
28 Aug 17 |
nicklas |
617 |
private final Object[] data; |
4553 |
28 Aug 17 |
nicklas |
618 |
private final String thisSubtype; |
5220 |
11 Jan 19 |
nicklas |
619 |
private final String parentSubtype; |
4769 |
18 Apr 18 |
nicklas |
620 |
private final List<ColumnWriter> colWriters; |
4553 |
28 Aug 17 |
nicklas |
621 |
|
5510 |
18 Jun 19 |
nicklas |
622 |
CohortTableWriter(DbControl dc, Item itemType, String subtype, String parentSubtype, String topSubtype, TableWriter out) |
4553 |
28 Aug 17 |
nicklas |
623 |
{ |
5510 |
18 Jun 19 |
nicklas |
624 |
this.out = out; |
4553 |
28 Aug 17 |
nicklas |
625 |
this.thisSubtype = subtype; |
5220 |
11 Jan 19 |
nicklas |
626 |
this.parentSubtype = parentSubtype; |
4553 |
28 Aug 17 |
nicklas |
627 |
|
5510 |
18 Jun 19 |
nicklas |
628 |
out.setDataSeparator("\t"); // Tab-separated file |
5510 |
18 Jun 19 |
nicklas |
629 |
out.setNullValue(""); |
4553 |
28 Aug 17 |
nicklas |
// Get rid of "bad" characters (tab, newline, etc.) |
5510 |
18 Jun 19 |
nicklas |
631 |
out.setEncoder(new ToSpaceEncoderDecoder()); |
4553 |
28 Aug 17 |
nicklas |
632 |
|
4769 |
18 Apr 18 |
nicklas |
// Create column writers |
4769 |
18 Apr 18 |
nicklas |
634 |
this.colWriters = new ArrayList<>(); |
5220 |
11 Jan 19 |
nicklas |
// The name of the current item |
5228 |
14 Jan 19 |
nicklas |
636 |
colWriters.add(new ItemNameColumn(thisSubtype + "Name")); |
5220 |
11 Jan 19 |
nicklas |
637 |
if (parentSubtype != null) |
4769 |
18 Apr 18 |
nicklas |
638 |
{ |
5220 |
11 Jan 19 |
nicklas |
// The name of the parent item |
5228 |
14 Jan 19 |
nicklas |
640 |
colWriters.add(new ParentItemNameColumn(parentSubtype + "Name")); |
5220 |
11 Jan 19 |
nicklas |
641 |
} |
5227 |
14 Jan 19 |
nicklas |
642 |
if (topSubtype != null && !topSubtype.equals(parentSubtype) && !topSubtype.equals(subtype)) |
5227 |
14 Jan 19 |
nicklas |
643 |
{ |
5227 |
14 Jan 19 |
nicklas |
// The name of the top item |
5228 |
14 Jan 19 |
nicklas |
645 |
colWriters.add(new TopItemNameColumn(topSubtype + "Name")); |
5227 |
14 Jan 19 |
nicklas |
646 |
} |
4769 |
18 Apr 18 |
nicklas |
647 |
|
5239 |
17 Jan 19 |
nicklas |
// The FirstReleasedIn annotation |
5239 |
17 Jan 19 |
nicklas |
649 |
AnnotationType firstReleasedIn = Annotationtype.FIRST_RELEASED_IN.load(dc); |
5239 |
17 Jan 19 |
nicklas |
650 |
colWriters.add(new AnnotationColumn(firstReleasedIn)); |
5239 |
17 Jan 19 |
nicklas |
651 |
|
4769 |
18 Apr 18 |
nicklas |
// Then, all annotation types defined for the current subtype |
4553 |
28 Aug 17 |
nicklas |
653 |
ItemQuery<AnnotationType> query = AnnotationType.getQuery(itemType); |
4553 |
28 Aug 17 |
nicklas |
654 |
query.setIncludes(Relax.INCLUDE_IN_CURRENT_PROJECT); |
5223 |
11 Jan 19 |
nicklas |
655 |
if (subtype != null) |
4553 |
28 Aug 17 |
nicklas |
656 |
{ |
4553 |
28 Aug 17 |
nicklas |
657 |
query.join(Hql.innerJoin("categories", "cat")); |
4553 |
28 Aug 17 |
nicklas |
658 |
query.restrict(Restrictions.eq(Hql.property("cat", "name"), Expressions.string(subtype))); |
4553 |
28 Aug 17 |
nicklas |
659 |
} |
4553 |
28 Aug 17 |
nicklas |
660 |
query.order(Orders.asc(Hql.property("name"))); |
4553 |
28 Aug 17 |
nicklas |
661 |
List<AnnotationType> list = query.list(dc); |
4553 |
28 Aug 17 |
nicklas |
662 |
for (AnnotationType at : list) |
4553 |
28 Aug 17 |
nicklas |
663 |
{ |
5239 |
17 Jan 19 |
nicklas |
664 |
if (at.equals(firstReleasedIn)) continue; // Avoid duplicate columns |
4769 |
18 Apr 18 |
nicklas |
665 |
colWriters.add(new AnnotationColumn(at)); |
4553 |
28 Aug 17 |
nicklas |
666 |
} |
4769 |
18 Apr 18 |
nicklas |
667 |
|
4769 |
18 Apr 18 |
nicklas |
668 |
data = new Object[colWriters.size()]; |
4769 |
18 Apr 18 |
nicklas |
669 |
writeHeaders(); |
4553 |
28 Aug 17 |
nicklas |
670 |
} |
4553 |
28 Aug 17 |
nicklas |
671 |
|
4769 |
18 Apr 18 |
nicklas |
672 |
/** |
4769 |
18 Apr 18 |
nicklas |
Write the headers. |
4769 |
18 Apr 18 |
nicklas |
674 |
*/ |
4769 |
18 Apr 18 |
nicklas |
675 |
private void writeHeaders() |
4553 |
28 Aug 17 |
nicklas |
676 |
{ |
4553 |
28 Aug 17 |
nicklas |
677 |
int col = 0; |
4769 |
18 Apr 18 |
nicklas |
678 |
for (ColumnWriter colWriter : colWriters) |
4553 |
28 Aug 17 |
nicklas |
679 |
{ |
4769 |
18 Apr 18 |
nicklas |
680 |
data[col] = colWriter.getHeader(); |
4553 |
28 Aug 17 |
nicklas |
681 |
col++; |
4553 |
28 Aug 17 |
nicklas |
682 |
} |
5510 |
18 Jun 19 |
nicklas |
683 |
out.tablePrintHeaders(data); |
4553 |
28 Aug 17 |
nicklas |
684 |
} |
4553 |
28 Aug 17 |
nicklas |
685 |
|
4769 |
18 Apr 18 |
nicklas |
686 |
/** |
4769 |
18 Apr 18 |
nicklas |
Write values for the current item. |
4769 |
18 Apr 18 |
nicklas |
688 |
*/ |
5227 |
14 Jan 19 |
nicklas |
689 |
void writeItem(CohortItem item, CohortChain chain, DbControl dc, SnapshotManager manager) |
4553 |
28 Aug 17 |
nicklas |
690 |
{ |
4553 |
28 Aug 17 |
nicklas |
691 |
int col = 0; |
4769 |
18 Apr 18 |
nicklas |
692 |
for (ColumnWriter colWriter : colWriters) |
4553 |
28 Aug 17 |
nicklas |
693 |
{ |
5227 |
14 Jan 19 |
nicklas |
694 |
data[col] = colWriter.getValue(item, chain, dc, manager); |
4553 |
28 Aug 17 |
nicklas |
695 |
col++; |
4553 |
28 Aug 17 |
nicklas |
696 |
} |
5510 |
18 Jun 19 |
nicklas |
697 |
out.tablePrintData(data); |
4553 |
28 Aug 17 |
nicklas |
698 |
} |
5510 |
18 Jun 19 |
nicklas |
699 |
|
5510 |
18 Jun 19 |
nicklas |
700 |
@Override |
5510 |
18 Jun 19 |
nicklas |
701 |
public void close() |
5510 |
18 Jun 19 |
nicklas |
702 |
{ |
5510 |
18 Jun 19 |
nicklas |
703 |
out.close(); |
5510 |
18 Jun 19 |
nicklas |
704 |
} |
4553 |
28 Aug 17 |
nicklas |
705 |
|
4553 |
28 Aug 17 |
nicklas |
706 |
} |
4553 |
28 Aug 17 |
nicklas |
707 |
|
4769 |
18 Apr 18 |
nicklas |
708 |
/** |
4769 |
18 Apr 18 |
nicklas |
Get value for an exported column. |
4769 |
18 Apr 18 |
nicklas |
710 |
*/ |
4769 |
18 Apr 18 |
nicklas |
711 |
static interface ColumnWriter |
4769 |
18 Apr 18 |
nicklas |
712 |
{ |
4769 |
18 Apr 18 |
nicklas |
713 |
/** |
4769 |
18 Apr 18 |
nicklas |
Column header. |
4769 |
18 Apr 18 |
nicklas |
715 |
*/ |
4769 |
18 Apr 18 |
nicklas |
716 |
String getHeader(); |
4769 |
18 Apr 18 |
nicklas |
717 |
|
4769 |
18 Apr 18 |
nicklas |
718 |
/** |
5227 |
14 Jan 19 |
nicklas |
Get the value from the specified item in the chain. |
4769 |
18 Apr 18 |
nicklas |
720 |
*/ |
5227 |
14 Jan 19 |
nicklas |
721 |
Object getValue(CohortItem item, CohortChain chain, DbControl dc, SnapshotManager manager); |
4769 |
18 Apr 18 |
nicklas |
722 |
} |
4769 |
18 Apr 18 |
nicklas |
723 |
|
4769 |
18 Apr 18 |
nicklas |
724 |
/** |
4769 |
18 Apr 18 |
nicklas |
Get the name of the current item. |
4769 |
18 Apr 18 |
nicklas |
726 |
*/ |
4769 |
18 Apr 18 |
nicklas |
727 |
static class ItemNameColumn |
4769 |
18 Apr 18 |
nicklas |
728 |
implements ColumnWriter |
4769 |
18 Apr 18 |
nicklas |
729 |
{ |
4769 |
18 Apr 18 |
nicklas |
730 |
private final String header; |
4769 |
18 Apr 18 |
nicklas |
731 |
ItemNameColumn(String header) |
4769 |
18 Apr 18 |
nicklas |
732 |
{ |
4769 |
18 Apr 18 |
nicklas |
733 |
this.header = header; |
4769 |
18 Apr 18 |
nicklas |
734 |
} |
4769 |
18 Apr 18 |
nicklas |
735 |
|
4769 |
18 Apr 18 |
nicklas |
736 |
@Override |
4769 |
18 Apr 18 |
nicklas |
737 |
public String getHeader() |
4769 |
18 Apr 18 |
nicklas |
738 |
{ |
4769 |
18 Apr 18 |
nicklas |
739 |
return header; |
4769 |
18 Apr 18 |
nicklas |
740 |
} |
4769 |
18 Apr 18 |
nicklas |
741 |
|
4769 |
18 Apr 18 |
nicklas |
742 |
@Override |
5227 |
14 Jan 19 |
nicklas |
743 |
public Object getValue(CohortItem item, CohortChain chain, DbControl dc, SnapshotManager manager) |
4769 |
18 Apr 18 |
nicklas |
744 |
{ |
4769 |
18 Apr 18 |
nicklas |
745 |
return item.getName(); |
4769 |
18 Apr 18 |
nicklas |
746 |
} |
4769 |
18 Apr 18 |
nicklas |
747 |
} |
4769 |
18 Apr 18 |
nicklas |
748 |
|
4769 |
18 Apr 18 |
nicklas |
749 |
/** |
4769 |
18 Apr 18 |
nicklas |
Get the name of the root item. |
4769 |
18 Apr 18 |
nicklas |
751 |
*/ |
4769 |
18 Apr 18 |
nicklas |
752 |
static class RootItemNameColumn |
4769 |
18 Apr 18 |
nicklas |
753 |
extends ItemNameColumn |
4769 |
18 Apr 18 |
nicklas |
754 |
{ |
4769 |
18 Apr 18 |
nicklas |
755 |
RootItemNameColumn(String header) |
4769 |
18 Apr 18 |
nicklas |
756 |
{ |
4769 |
18 Apr 18 |
nicklas |
757 |
super(header); |
4769 |
18 Apr 18 |
nicklas |
758 |
} |
4769 |
18 Apr 18 |
nicklas |
759 |
|
4769 |
18 Apr 18 |
nicklas |
760 |
@Override |
5227 |
14 Jan 19 |
nicklas |
761 |
public Object getValue(CohortItem item, CohortChain chain, DbControl dc, SnapshotManager manager) |
4769 |
18 Apr 18 |
nicklas |
762 |
{ |
5227 |
14 Jan 19 |
nicklas |
763 |
return chain.getRootItem().getName(); |
4769 |
18 Apr 18 |
nicklas |
764 |
} |
4769 |
18 Apr 18 |
nicklas |
765 |
} |
5227 |
14 Jan 19 |
nicklas |
766 |
|
4769 |
18 Apr 18 |
nicklas |
767 |
/** |
5227 |
14 Jan 19 |
nicklas |
Get the name of the top item (typically the Patient). |
5220 |
11 Jan 19 |
nicklas |
769 |
*/ |
5227 |
14 Jan 19 |
nicklas |
770 |
static class TopItemNameColumn |
5220 |
11 Jan 19 |
nicklas |
771 |
extends ItemNameColumn |
5220 |
11 Jan 19 |
nicklas |
772 |
{ |
5227 |
14 Jan 19 |
nicklas |
773 |
TopItemNameColumn(String header) |
5220 |
11 Jan 19 |
nicklas |
774 |
{ |
5220 |
11 Jan 19 |
nicklas |
775 |
super(header); |
5220 |
11 Jan 19 |
nicklas |
776 |
} |
5220 |
11 Jan 19 |
nicklas |
777 |
|
5220 |
11 Jan 19 |
nicklas |
778 |
@Override |
5227 |
14 Jan 19 |
nicklas |
779 |
public Object getValue(CohortItem item, CohortChain chain, DbControl dc, SnapshotManager manager) |
5220 |
11 Jan 19 |
nicklas |
780 |
{ |
5227 |
14 Jan 19 |
nicklas |
781 |
return chain.getTopItem().getName(); |
5220 |
11 Jan 19 |
nicklas |
782 |
} |
5220 |
11 Jan 19 |
nicklas |
783 |
} |
5227 |
14 Jan 19 |
nicklas |
784 |
|
5220 |
11 Jan 19 |
nicklas |
785 |
/** |
5227 |
14 Jan 19 |
nicklas |
Get the name of the parent item. |
5220 |
11 Jan 19 |
nicklas |
787 |
*/ |
5227 |
14 Jan 19 |
nicklas |
788 |
static class ParentItemNameColumn |
5220 |
11 Jan 19 |
nicklas |
789 |
extends ItemNameColumn |
5220 |
11 Jan 19 |
nicklas |
790 |
{ |
5227 |
14 Jan 19 |
nicklas |
791 |
ParentItemNameColumn(String header) |
5220 |
11 Jan 19 |
nicklas |
792 |
{ |
5227 |
14 Jan 19 |
nicklas |
793 |
super(header); |
5220 |
11 Jan 19 |
nicklas |
794 |
} |
5220 |
11 Jan 19 |
nicklas |
795 |
|
5220 |
11 Jan 19 |
nicklas |
796 |
@Override |
5227 |
14 Jan 19 |
nicklas |
797 |
public Object getValue(CohortItem item, CohortChain chain, DbControl dc, SnapshotManager manager) |
5220 |
11 Jan 19 |
nicklas |
798 |
{ |
5227 |
14 Jan 19 |
nicklas |
799 |
return item.getParentItem(dc).getName(); |
5220 |
11 Jan 19 |
nicklas |
800 |
} |
5220 |
11 Jan 19 |
nicklas |
801 |
} |
5220 |
11 Jan 19 |
nicklas |
802 |
|
5220 |
11 Jan 19 |
nicklas |
803 |
/** |
4769 |
18 Apr 18 |
nicklas |
Get value for a specified annotation type. |
4769 |
18 Apr 18 |
nicklas |
805 |
*/ |
4769 |
18 Apr 18 |
nicklas |
806 |
static class AnnotationColumn |
4769 |
18 Apr 18 |
nicklas |
807 |
implements ColumnWriter |
4769 |
18 Apr 18 |
nicklas |
808 |
{ |
4769 |
18 Apr 18 |
nicklas |
809 |
private final AnnotationType at; |
4769 |
18 Apr 18 |
nicklas |
810 |
private final AnnotationTypeFilter filter; |
4769 |
18 Apr 18 |
nicklas |
811 |
|
4769 |
18 Apr 18 |
nicklas |
812 |
AnnotationColumn(AnnotationType at) |
4769 |
18 Apr 18 |
nicklas |
813 |
{ |
4769 |
18 Apr 18 |
nicklas |
814 |
this.at = at; |
4769 |
18 Apr 18 |
nicklas |
815 |
this.filter = new AnnotationTypeFilter(at); |
4769 |
18 Apr 18 |
nicklas |
816 |
} |
4769 |
18 Apr 18 |
nicklas |
817 |
|
4769 |
18 Apr 18 |
nicklas |
818 |
@Override |
4769 |
18 Apr 18 |
nicklas |
819 |
public String getHeader() |
4769 |
18 Apr 18 |
nicklas |
820 |
{ |
4769 |
18 Apr 18 |
nicklas |
821 |
return at.getName(); |
4769 |
18 Apr 18 |
nicklas |
822 |
} |
4769 |
18 Apr 18 |
nicklas |
823 |
|
4769 |
18 Apr 18 |
nicklas |
824 |
@Override |
5227 |
14 Jan 19 |
nicklas |
825 |
public Object getValue(CohortItem item, CohortChain chain, DbControl dc, SnapshotManager manager) |
4769 |
18 Apr 18 |
nicklas |
826 |
{ |
4769 |
18 Apr 18 |
nicklas |
827 |
List<AnnotationSnapshot> snapshots = manager.findAnnotations(dc, (Annotatable)item.getItem(), filter, false); |
4769 |
18 Apr 18 |
nicklas |
828 |
Object value = snapshots.size() > 0 ? snapshots.get(0).getThisValues().get(0) : null; |
4769 |
18 Apr 18 |
nicklas |
829 |
return value; |
4769 |
18 Apr 18 |
nicklas |
830 |
} |
4769 |
18 Apr 18 |
nicklas |
831 |
|
4769 |
18 Apr 18 |
nicklas |
832 |
} |
4553 |
28 Aug 17 |
nicklas |
833 |
} |