PY-19960 Log validation failures
[idea/community.git] / python / ipnb / src / org / jetbrains / plugins / ipnb / format / IpnbParser.java
1 package org.jetbrains.plugins.ipnb.format;
2
3 import com.google.common.collect.Lists;
4 import com.google.gson.*;
5 import com.google.gson.annotations.SerializedName;
6 import com.google.gson.stream.JsonWriter;
7 import com.intellij.openapi.diagnostic.Logger;
8 import com.intellij.openapi.editor.Document;
9 import com.intellij.openapi.module.Module;
10 import com.intellij.openapi.project.Project;
11 import com.intellij.openapi.project.ProjectUtil;
12 import com.intellij.openapi.projectRoots.Sdk;
13 import com.intellij.openapi.roots.ProjectRootManager;
14 import com.intellij.openapi.ui.Messages;
15 import com.intellij.openapi.util.text.StringUtil;
16 import com.intellij.openapi.vfs.VirtualFile;
17 import com.intellij.util.text.VersionComparatorUtil;
18 import com.jetbrains.python.packaging.PyPackage;
19 import com.jetbrains.python.packaging.PyPackageUtil;
20 import com.jetbrains.python.sdk.PythonSdkType;
21 import org.jetbrains.annotations.NotNull;
22 import org.jetbrains.annotations.Nullable;
23 import org.jetbrains.plugins.ipnb.editor.panels.IpnbEditablePanel;
24 import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel;
25 import org.jetbrains.plugins.ipnb.format.cells.*;
26 import org.jetbrains.plugins.ipnb.format.cells.output.*;
27
28 import java.io.*;
29 import java.lang.reflect.Type;
30 import java.nio.charset.Charset;
31 import java.util.*;
32
33 public class IpnbParser {
34   private static final Logger LOG = Logger.getInstance(IpnbParser.class);
35   private static final Gson gson = initGson();
36   private static final List<String> myErrors = new ArrayList<>();
37   private static final String VALIDATION_ERROR_TEXT = "An invalid notebook may not function properly. The validation error was:";
38   private static final String VALIDATION_ERROR_TITLE = "Notebook Validation Failed";
39
40   @NotNull
41   private static Gson initGson() {
42     final GsonBuilder builder =
43       new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().registerTypeAdapter(IpnbCellRaw.class, new RawCellAdapter())
44         .registerTypeAdapter(IpnbFileRaw.class, new FileAdapter()).registerTypeAdapter(CellOutputRaw.class, new OutputsAdapter())
45         .registerTypeAdapter(OutputDataRaw.class, new OutputDataAdapter())
46         .registerTypeAdapter(CellOutputRaw.class, new CellOutputDeserializer())
47         .registerTypeAdapter(OutputDataRaw.class, new OutputDataDeserializer())
48         .registerTypeAdapter(IpnbCellRaw.class, new CellRawDeserializer()).serializeNulls();
49     return builder.create();
50   }
51
52   @NotNull
53   public static IpnbFile parseIpnbFile(@NotNull final CharSequence fileText, @NotNull final VirtualFile virtualFile) throws IOException {
54     myErrors.clear();
55     
56     final String path = virtualFile.getPath();
57     IpnbFileRaw rawFile = gson.fromJson(fileText.toString(), IpnbFileRaw.class);
58     if (rawFile == null) {
59       int nbformat = isIpythonNewFormat(virtualFile) ? 4 : 3;
60       return new IpnbFile(Collections.emptyMap(), nbformat, Lists.newArrayList(), path);
61     }
62     List<IpnbCell> cells = new ArrayList<>();
63     final IpnbWorksheet[] worksheets = rawFile.worksheets;
64     if (worksheets == null) {
65       for (IpnbCellRaw rawCell : rawFile.cells) {
66         cells.add(rawCell.createCell(validateSource(rawCell)));
67       }
68     }
69     else {
70       for (IpnbWorksheet worksheet : worksheets) {
71         final List<IpnbCellRaw> rawCells = worksheet.cells;
72         for (IpnbCellRaw rawCell : rawCells) {
73           cells.add(rawCell.createCell(validateSource(rawCell)));
74         }
75       }
76     }
77     showValidationMessage();
78     return new IpnbFile(rawFile.metadata, rawFile.nbformat, cells, path);
79   }
80
81   private static boolean validateSource(IpnbCellRaw cell) {
82     if (cell.source == null) {
83       final String error = VALIDATION_ERROR_TEXT + "\n" + "\"source\" is required property:\n" + cell;
84       myErrors.add(error);
85       LOG.warn(error);
86       return false;
87     }
88     return true;
89   }
90   
91   private static void showValidationMessage() {
92     if (!myErrors.isEmpty()) {
93       Messages.showWarningDialog(myErrors.get(0), VALIDATION_ERROR_TITLE);
94     }
95   }
96
97   public static boolean isIpythonNewFormat(@NotNull final VirtualFile virtualFile) {
98     final Project project = ProjectUtil.guessProjectForFile(virtualFile);
99     if (project != null) {
100       final Module module = ProjectRootManager.getInstance(project).getFileIndex().getModuleForFile(virtualFile);
101       if (module != null) {
102         final Sdk sdk = PythonSdkType.findPythonSdk(module);
103         if (sdk != null) {
104           // It should be called first before IpnbConnectionManager#startIpythonServer()
105           final List<PyPackage> packages = PyPackageUtil.refreshAndGetPackagesModally(sdk);
106           final PyPackage ipython = packages != null ? PyPackageUtil.findPackage(packages, "ipython") : null;
107           final PyPackage jupyter = packages != null ? PyPackageUtil.findPackage(packages, "jupyter") : null;
108           if (jupyter == null && ipython != null && VersionComparatorUtil.compare(ipython.getVersion(), "3.0") <= 0) {
109             return false;
110           }
111         }
112       }
113     }
114     return true;
115   }
116
117   @NotNull
118   public static IpnbFile parseIpnbFile(@NotNull Document document, @NotNull final VirtualFile virtualFile) throws IOException {
119     return parseIpnbFile(document.getImmutableCharSequence(), virtualFile);
120   }
121
122   public static void saveIpnbFile(@NotNull final IpnbFilePanel ipnbPanel) {
123     final String json = newDocumentText(ipnbPanel);
124     if (json == null) return;
125     writeToFile(ipnbPanel.getIpnbFile().getPath(), json);
126   }
127
128   @Nullable
129   public static String newDocumentText(@NotNull final IpnbFilePanel ipnbPanel) {
130     final IpnbFile ipnbFile = ipnbPanel.getIpnbFile();
131     if (ipnbFile == null) return null;
132     for (IpnbEditablePanel panel : ipnbPanel.getIpnbPanels()) {
133       if (panel.isModified()) {
134         panel.updateCellSource();
135       }
136     }
137
138     final IpnbFileRaw fileRaw = new IpnbFileRaw();
139     fileRaw.metadata = ipnbFile.getMetadata();
140     if (ipnbFile.getNbformat() == 4) {
141       for (IpnbCell cell: ipnbFile.getCells()) {
142         fileRaw.cells.add(IpnbCellRaw.fromCell(cell, ipnbFile.getNbformat()));
143       }
144     }
145     else {
146       final IpnbWorksheet worksheet = new IpnbWorksheet();
147       worksheet.cells.clear();
148       for (IpnbCell cell : ipnbFile.getCells()) {
149         worksheet.cells.add(IpnbCellRaw.fromCell(cell, ipnbFile.getNbformat()));
150       }
151       fileRaw.worksheets = new IpnbWorksheet[]{worksheet};
152     }
153     final StringWriter stringWriter = new StringWriter();
154     final JsonWriter writer = new JsonWriter(stringWriter);
155     writer.setIndent(" ");
156     gson.toJson(fileRaw, fileRaw.getClass(), writer);
157     return stringWriter.toString();
158   }
159
160   private static void writeToFile(@NotNull final String path, @NotNull final String json) {
161     final File file = new File(path);
162     try {
163       final FileOutputStream fileOutputStream = new FileOutputStream(file);
164       final OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream, Charset.forName("UTF-8").newEncoder());
165       try {
166         writer.write(json);
167       } catch (IOException e) {
168         LOG.error(e);
169       }
170       finally {
171         try {
172           writer.close();
173           fileOutputStream.close();
174         } catch (IOException e) {
175           LOG.error(e);
176         }
177       }
178     }
179     catch (FileNotFoundException e) {
180       LOG.error(e);
181     }
182   }
183
184   @SuppressWarnings("unused")
185   public static class IpnbFileRaw {
186     IpnbWorksheet[] worksheets;
187     List<IpnbCellRaw> cells = new ArrayList<>();
188     Map<String, Object> metadata = new HashMap<>();
189     int nbformat = 4;
190     int nbformat_minor;
191   }
192
193   private static class IpnbWorksheet {
194     List<IpnbCellRaw> cells = new ArrayList<>();
195   }
196
197   @SuppressWarnings("unused")
198   private static class IpnbCellRaw {
199     String cell_type;
200     Integer execution_count;
201     Map<String, Object> metadata = new HashMap<>();
202     Integer level;
203     List<CellOutputRaw> outputs;
204     List<String> source;
205     List<String> input;
206     String language;
207     Integer prompt_number;
208
209     @Override
210     public String toString() {
211       return new GsonBuilder().setPrettyPrinting().create().toJson(this);
212     }
213
214     public static IpnbCellRaw fromCell(@NotNull final IpnbCell cell, int nbformat) {
215       final IpnbCellRaw raw = new IpnbCellRaw();
216       if (cell instanceof IpnbEditableCell) {
217         raw.metadata = ((IpnbEditableCell)cell).getMetadata();
218       }
219       if (cell instanceof IpnbMarkdownCell) {
220         raw.cell_type = "markdown";
221         raw.source = ((IpnbMarkdownCell)cell).getSource();
222       }
223       else if (cell instanceof IpnbCodeCell) {
224         raw.cell_type = "code";
225         final ArrayList<CellOutputRaw> outputRaws = new ArrayList<>();
226         for (IpnbOutputCell outputCell : ((IpnbCodeCell)cell).getCellOutputs()) {
227           outputRaws.add(CellOutputRaw.fromOutput(outputCell, nbformat));
228         }
229         raw.outputs = outputRaws;
230         final Integer promptNumber = ((IpnbCodeCell)cell).getPromptNumber();
231         if (nbformat == 4) {
232           raw.execution_count = promptNumber != null && promptNumber >= 0 ? promptNumber : null;
233           raw.source = ((IpnbCodeCell)cell).getSource();
234         }
235         else {
236           raw.prompt_number = promptNumber != null && promptNumber >= 0 ? promptNumber : null;
237           raw.language = ((IpnbCodeCell)cell).getLanguage();
238           raw.input = ((IpnbCodeCell)cell).getSource();
239         }
240       }
241       else if (cell instanceof IpnbRawCell) {
242         raw.cell_type = "raw";
243         raw.source = ((IpnbRawCell)cell).getSource();
244       }
245       else if (cell instanceof IpnbHeadingCell) {
246         raw.cell_type = "heading";
247         raw.source = ((IpnbHeadingCell)cell).getSource();
248         raw.level = ((IpnbHeadingCell)cell).getLevel();
249       }
250       return raw;
251     }
252
253     @Nullable
254     public IpnbCell createCell(boolean isValidSource) {
255       final IpnbCell cell;
256       if (cell_type.equals("markdown")) {
257         cell = new IpnbMarkdownCell(isValidSource ? source : Collections.emptyList(), metadata);
258       }
259       else if (cell_type.equals("code")) {
260         final List<IpnbOutputCell> outputCells = new ArrayList<>();
261         for (CellOutputRaw outputRaw : outputs) {
262           outputCells.add(outputRaw.createOutput());
263         }
264         final Integer prompt = prompt_number != null ? prompt_number : execution_count;
265         cell = new IpnbCodeCell(language == null ? "python" : language, 
266                                 input == null ?  (isValidSource ? source : Collections.emptyList()) : input, 
267                                 prompt, outputCells, metadata);
268         ;
269       }
270       else if (cell_type.equals("raw")) {
271         cell = new IpnbRawCell(isValidSource ? source : Collections.emptyList());
272       }
273       else if (cell_type.equals("heading")) {
274         cell = new IpnbHeadingCell(isValidSource ? source : Collections.emptyList(), level, metadata);
275       }
276       else {
277         cell = null;
278       }
279       return cell;
280     }
281   }
282
283   private static class CellOutputRaw {
284     String ename;
285     String name;
286     String evalue;
287     OutputDataRaw data;
288     Integer execution_count;
289     String png;
290     String stream;
291     String jpeg;
292     List<String> html;
293     List<String> latex;
294     List<String> svg;
295     Integer prompt_number;
296     List<String> traceback;
297     Map<String, Object> metadata;
298     String output_type;
299     List<String> text;
300
301     public static CellOutputRaw fromOutput(@NotNull final IpnbOutputCell outputCell, int nbformat) {
302       final CellOutputRaw raw = new CellOutputRaw();
303       raw.metadata = outputCell.getMetadata();
304       if (raw.metadata == null && !(outputCell instanceof IpnbStreamOutputCell) && !(outputCell instanceof IpnbErrorOutputCell)) {
305         raw.metadata = Collections.emptyMap();
306       }
307
308       if (outputCell instanceof IpnbPngOutputCell) {
309         if (nbformat == 4) {
310           final OutputDataRaw dataRaw = new OutputDataRaw();
311           dataRaw.png = ((IpnbPngOutputCell)outputCell).getBase64String();
312           dataRaw.text = outputCell.getText();
313           raw.data = dataRaw;
314           raw.execution_count = outputCell.getPromptNumber();
315           raw.output_type = outputCell.getPromptNumber() != null ? "execute_result" : "display_data";
316         }
317         else {
318           raw.png = ((IpnbPngOutputCell)outputCell).getBase64String();
319           raw.text = outputCell.getText();
320         }
321       }
322       else if (outputCell instanceof IpnbSvgOutputCell) {
323         if (nbformat == 4) {
324           final OutputDataRaw dataRaw = new OutputDataRaw();
325           dataRaw.text = outputCell.getText();
326           dataRaw.svg = ((IpnbSvgOutputCell)outputCell).getSvg();
327           raw.data = dataRaw;
328           raw.execution_count = outputCell.getPromptNumber();
329           raw.output_type = outputCell.getPromptNumber() != null ? "execute_result" : "display_data";
330         }
331         else {
332           raw.svg = ((IpnbSvgOutputCell)outputCell).getSvg();
333           raw.text = outputCell.getText();
334         }
335       }
336       else if (outputCell instanceof IpnbJpegOutputCell) {
337         if (nbformat == 4) {
338           final OutputDataRaw dataRaw = new OutputDataRaw();
339           dataRaw.text = outputCell.getText();
340           dataRaw.jpeg = Lists.newArrayList(((IpnbJpegOutputCell)outputCell).getBase64String());
341           raw.data = dataRaw;
342         }
343         else {
344           raw.jpeg = ((IpnbJpegOutputCell)outputCell).getBase64String();
345           raw.text = outputCell.getText();
346         }
347       }
348       else if (outputCell instanceof IpnbLatexOutputCell) {
349         if (nbformat == 4) {
350           final OutputDataRaw dataRaw = new OutputDataRaw();
351           dataRaw.text = outputCell.getText();
352           dataRaw.latex = ((IpnbLatexOutputCell)outputCell).getLatex();
353           raw.data = dataRaw;
354           raw.execution_count = outputCell.getPromptNumber();
355           raw.output_type = "execute_result";
356         }
357         else {
358           raw.latex = ((IpnbLatexOutputCell)outputCell).getLatex();
359           raw.text = outputCell.getText();
360           raw.prompt_number = outputCell.getPromptNumber();
361         }
362       }
363       else if (outputCell instanceof IpnbStreamOutputCell) {
364         if (nbformat == 4) {
365           raw.name = ((IpnbStreamOutputCell)outputCell).getStream();
366         }
367         else {
368           raw.stream = ((IpnbStreamOutputCell)outputCell).getStream();
369         }
370         raw.text = outputCell.getText();
371         raw.output_type = "stream";
372       }
373       else if (outputCell instanceof IpnbHtmlOutputCell) {
374         if (nbformat == 4) {
375           final OutputDataRaw dataRaw = new OutputDataRaw();
376           dataRaw.html = ((IpnbHtmlOutputCell)outputCell).getHtmls();
377           dataRaw.text = outputCell.getText();
378           raw.data = dataRaw;
379           raw.execution_count = outputCell.getPromptNumber();
380         }
381         else {
382           raw.html = ((IpnbHtmlOutputCell)outputCell).getHtmls();
383         }
384         raw.output_type = nbformat == 4 ? "execute_result" : "pyout";
385       }
386       else if (outputCell instanceof IpnbErrorOutputCell) {
387         raw.output_type = nbformat == 4 ? "error" : "pyerr";
388         raw.evalue = ((IpnbErrorOutputCell)outputCell).getEvalue();
389         raw.ename = ((IpnbErrorOutputCell)outputCell).getEname();
390         raw.traceback = outputCell.getText();
391       }
392       else if (outputCell instanceof IpnbOutOutputCell) {
393         if (nbformat == 4) {
394           raw.execution_count = outputCell.getPromptNumber();
395           raw.output_type = "execute_result";
396           final OutputDataRaw dataRaw = new OutputDataRaw();
397           dataRaw.text = outputCell.getText();
398           raw.data = dataRaw;
399         }
400         else {
401           raw.output_type = "pyout";
402           raw.prompt_number = outputCell.getPromptNumber();
403           raw.text = outputCell.getText();
404         }
405       }
406       else {
407         raw.text = outputCell.getText();
408       }
409       return raw;
410     }
411
412     public IpnbOutputCell createOutput() {
413       List<String> text = this.text != null ? this.text : data != null ? data.text : Lists.newArrayList();
414       Integer prompt = execution_count != null ? execution_count : prompt_number;
415       final IpnbOutputCell outputCell;
416       if (png != null || (data != null && data.png != null)) {
417         outputCell = new IpnbPngOutputCell(png == null ? StringUtil.join(data.png) : png, text, prompt, metadata);
418       }
419       else if (jpeg != null || (data != null && data.jpeg != null)) {
420         outputCell = new IpnbJpegOutputCell(jpeg == null ? StringUtil.join(data.jpeg, "") : jpeg, text, prompt, metadata);
421       }
422       else if (svg != null || (data != null && data.svg != null)) {
423         outputCell = new IpnbSvgOutputCell(svg == null ? data.svg : svg, text, prompt, metadata);
424       }
425       else if (html != null || (data != null && data.html != null)) {
426         outputCell = new IpnbHtmlOutputCell(html == null ? data.html : html, text, prompt, metadata);
427       }
428       else if (latex != null || (data != null && data.latex != null)) {
429         outputCell = new IpnbLatexOutputCell(latex == null ? data.latex : latex, prompt, text, metadata);
430       }
431       else if (stream != null || name != null) {
432         outputCell = new IpnbStreamOutputCell(stream == null ? name : stream, text, prompt, metadata);
433       }
434       else if ("pyerr".equals(output_type) || "error".equals(output_type)) {
435         outputCell = new IpnbErrorOutputCell(evalue, ename, traceback, prompt, metadata);
436       }
437       else if ("pyout".equals(output_type)) {
438         outputCell = new IpnbOutOutputCell(text, prompt, metadata);
439       }
440       else if ("execute_result".equals(output_type) && data != null) {
441         outputCell = new IpnbOutOutputCell(data.text, prompt, metadata);
442       }
443       else if ("display_data".equals(output_type)){
444         outputCell = new IpnbPngOutputCell(null, text, prompt, metadata);
445       }
446       else {
447         outputCell = new IpnbOutputCell(text, prompt, metadata);
448       }
449       return outputCell;
450     }
451   }
452
453   private static class OutputDataRaw {
454     @SerializedName("image/png") String png;
455     @SerializedName("text/html") List<String> html;
456     @SerializedName("image/svg+xml") List<String> svg;
457     @SerializedName("image/jpeg") List<String> jpeg;
458     @SerializedName("text/latex") List<String> latex;
459     @SerializedName("text/plain") List<String> text;
460   }
461
462   static class RawCellAdapter implements JsonSerializer<IpnbCellRaw> {
463     @Override
464     public JsonElement serialize(IpnbCellRaw cellRaw, Type typeOfSrc, JsonSerializationContext context) {
465       final JsonObject jsonObject = new JsonObject();
466       jsonObject.addProperty("cell_type", cellRaw.cell_type);
467       if ("code".equals(cellRaw.cell_type)) {
468         final Integer count = cellRaw.execution_count;
469         if (count == null) {
470           jsonObject.add("execution_count", JsonNull.INSTANCE);
471         }
472         else {
473           jsonObject.addProperty("execution_count", count);
474         }
475       }
476       if (cellRaw.metadata != null) {
477         final JsonElement metadata = gson.toJsonTree(cellRaw.metadata);
478         jsonObject.add("metadata", metadata);
479       }
480       if (cellRaw.level != null) {
481         jsonObject.addProperty("level", cellRaw.level);
482       }
483
484       if (cellRaw.outputs != null) {
485         final JsonElement outputs = gson.toJsonTree(cellRaw.outputs);
486         jsonObject.add("outputs", outputs);
487       }
488       if (cellRaw.source != null) {
489         final JsonElement source = gson.toJsonTree(cellRaw.source);
490         jsonObject.add("source", source);
491       }
492       if (cellRaw.input != null) {
493         final JsonElement input = gson.toJsonTree(cellRaw.input);
494         jsonObject.add("input", input);
495       }
496       if (cellRaw.language != null) {
497         jsonObject.addProperty("language", cellRaw.language);
498       }
499       if (cellRaw.prompt_number != null) {
500         jsonObject.addProperty("prompt_number", cellRaw.prompt_number);
501       }
502
503       return jsonObject;
504     }
505   }
506   static class FileAdapter implements JsonSerializer<IpnbFileRaw> {
507     @Override
508     public JsonElement serialize(IpnbFileRaw fileRaw, Type typeOfSrc, JsonSerializationContext context) {
509       final JsonObject jsonObject = new JsonObject();
510       if (fileRaw.worksheets != null) {
511         final JsonElement worksheets = gson.toJsonTree(fileRaw.worksheets);
512         jsonObject.add("worksheets", worksheets);
513       }
514       if (fileRaw.cells != null) {
515         final JsonElement cells = gson.toJsonTree(fileRaw.cells);
516         jsonObject.add("cells", cells);
517       }
518       final JsonElement metadata = gson.toJsonTree(fileRaw.metadata);
519       jsonObject.add("metadata", metadata);
520
521       jsonObject.addProperty("nbformat", fileRaw.nbformat);
522       jsonObject.addProperty("nbformat_minor", fileRaw.nbformat_minor);
523
524       return jsonObject;
525     }
526   }
527
528   static class CellRawDeserializer implements JsonDeserializer<IpnbCellRaw> {
529
530     @Override
531     public IpnbCellRaw deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
532       throws JsonParseException {
533       final JsonObject object = json.getAsJsonObject();
534       final IpnbCellRaw cellRaw = new IpnbCellRaw();
535       final JsonElement cell_type = object.get("cell_type");
536       if (cell_type != null) {
537         cellRaw.cell_type = cell_type.getAsString();
538       }
539       final JsonElement count = object.get("execution_count");
540       if (count != null) {
541         cellRaw.execution_count = count.isJsonNull() ? null : count.getAsInt();
542       }
543       final JsonElement metadata = object.get("metadata");
544       if (metadata != null) {
545         cellRaw.metadata = gson.fromJson(metadata, Map.class);
546       }
547       final JsonElement level = object.get("level");
548       if (level != null) {
549         cellRaw.level = level.getAsInt();
550       }
551
552       final JsonElement outputsElement = object.get("outputs");
553       if (outputsElement != null) {
554         final JsonArray outputs = outputsElement.getAsJsonArray();
555         cellRaw.outputs = Lists.newArrayList();
556         for (JsonElement output : outputs) {
557           cellRaw.outputs.add(gson.fromJson(output, CellOutputRaw.class));
558         }
559       }
560       cellRaw.source = getStringOrArray("source", object);
561       cellRaw.input = getStringOrArray("input", object);
562       final JsonElement language = object.get("language");
563       if (language != null) {
564         cellRaw.language = language.getAsString();
565       }
566       final JsonElement number = object.get("prompt_number");
567       if (number != null) {
568         cellRaw.prompt_number = number.getAsInt();
569       }
570       return cellRaw;
571     }
572   }
573
574   static class OutputDataDeserializer implements JsonDeserializer<OutputDataRaw> {
575
576     @Override
577     public OutputDataRaw deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
578       throws JsonParseException {
579       final JsonObject object = json.getAsJsonObject();
580       final OutputDataRaw dataRaw = new OutputDataRaw();
581       final JsonElement png = object.get("image/png");
582       if (png != null) {
583         dataRaw.png = png.getAsString();
584       }
585       dataRaw.html = getStringOrArray("text/html", object);
586       dataRaw.svg = getStringOrArray("image/svg+xml", object);
587       dataRaw.jpeg = getStringOrArray("image/jpeg", object);
588       dataRaw.latex = getStringOrArray("text/latex", object);
589       dataRaw.text = getStringOrArray("text/plain", object);
590       return dataRaw;
591     }
592   }
593   static class CellOutputDeserializer implements JsonDeserializer<CellOutputRaw> {
594
595     @Override
596     public CellOutputRaw deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
597       throws JsonParseException {
598       final JsonObject object = json.getAsJsonObject();
599       final CellOutputRaw cellOutputRaw = new CellOutputRaw();
600       final JsonElement ename = object.get("ename");
601       if (ename != null) {
602         cellOutputRaw.ename = ename.getAsString();
603       }
604       final JsonElement name = object.get("name");
605       if (name != null) {
606         cellOutputRaw.name = name.getAsString();
607       }
608       final JsonElement evalue = object.get("evalue");
609       if (evalue != null) {
610         cellOutputRaw.evalue = evalue.getAsString();
611       }
612       final JsonElement data = object.get("data");
613       if (data != null) {
614         cellOutputRaw.data = gson.fromJson(data, OutputDataRaw.class);
615       }
616
617       final JsonElement count = object.get("execution_count");
618       if (count != null) {
619         cellOutputRaw.execution_count = count.getAsInt();
620       }
621       final JsonElement outputType = object.get("output_type");
622       if (outputType != null) {
623         cellOutputRaw.output_type = outputType.getAsString();
624       }
625       final JsonElement png = object.get("png");
626       if (png != null) {
627         cellOutputRaw.png = png.getAsString();
628       }
629       final JsonElement stream = object.get("stream");
630       if (stream != null) {
631         cellOutputRaw.stream = stream.getAsString();
632       }
633       final JsonElement jpeg = object.get("jpeg");
634       if (jpeg != null) {
635         cellOutputRaw.jpeg = jpeg.getAsString();
636       }
637
638       cellOutputRaw.html = getStringOrArray("html", object);
639       cellOutputRaw.latex = getStringOrArray("latex", object);
640       cellOutputRaw.svg = getStringOrArray("svg", object);
641       final JsonElement promptNumber = object.get("prompt_number");
642       if (promptNumber != null) {
643         cellOutputRaw.prompt_number = promptNumber.getAsInt();
644       }
645       cellOutputRaw.text = getStringOrArray("text", object);
646       cellOutputRaw.traceback = getStringOrArray("traceback", object);
647       final JsonElement metadata = object.get("metadata");
648       if (metadata != null) {
649         cellOutputRaw.metadata = gson.fromJson(metadata, Map.class);
650       }
651
652       return cellOutputRaw;
653     }
654   }
655
656   @Nullable
657   private static ArrayList<String> getStringOrArray(String name, JsonObject object) {
658     final JsonElement jsonElement = object.get(name);
659     final ArrayList<String> strings = Lists.newArrayList();
660     if (jsonElement == null) return null;
661     if (jsonElement.isJsonArray()) {
662       final JsonArray array = jsonElement.getAsJsonArray();
663       for (JsonElement element : array) {
664         strings.add(element.getAsString());
665       }
666     }
667     else {
668       strings.add(jsonElement.getAsString());
669     }
670     return strings;
671   }
672
673   static class OutputsAdapter implements JsonSerializer<CellOutputRaw> {
674     @Override
675     public JsonElement serialize(CellOutputRaw cellRaw, Type typeOfSrc, JsonSerializationContext context) {
676       final JsonObject jsonObject = new JsonObject();
677       if (cellRaw.ename != null) {
678         jsonObject.addProperty("ename", cellRaw.ename);
679       }
680       if (cellRaw.name != null) {
681         jsonObject.addProperty("name", cellRaw.name);
682       }
683       if (cellRaw.evalue != null) {
684         jsonObject.addProperty("evalue", cellRaw.evalue);
685       }
686
687       if (cellRaw.data != null) {
688         final JsonElement data = gson.toJsonTree(cellRaw.data);
689         jsonObject.add("data", data);
690       }
691       if (cellRaw.execution_count != null) {
692         jsonObject.addProperty("execution_count", cellRaw.execution_count);
693       }
694       if (cellRaw.png != null) {
695         jsonObject.addProperty("png", cellRaw.png);
696       }
697
698       if (cellRaw.stream != null) {
699         jsonObject.addProperty("stream", cellRaw.stream);
700       }
701
702       if (cellRaw.jpeg != null) {
703         jsonObject.addProperty("jpeg", cellRaw.jpeg);
704       }
705
706       if (cellRaw.html != null) {
707         final JsonElement html = gson.toJsonTree(cellRaw.html);
708         jsonObject.add("html", html);
709       }
710       if (cellRaw.latex != null) {
711         final JsonElement latex = gson.toJsonTree(cellRaw.latex);
712         jsonObject.add("latex", latex);
713       }
714
715       if (cellRaw.svg != null) {
716         final JsonElement svg = gson.toJsonTree(cellRaw.svg);
717         jsonObject.add("svg", svg);
718       }
719       if (cellRaw.prompt_number != null) {
720         jsonObject.addProperty("prompt_number", cellRaw.prompt_number);
721       }
722       if (cellRaw.traceback != null) {
723         final JsonElement traceback = gson.toJsonTree(cellRaw.traceback);
724         jsonObject.add("traceback", traceback);
725       }
726
727       if (cellRaw.metadata != null) {
728         final JsonElement metadata = gson.toJsonTree(cellRaw.metadata);
729         jsonObject.add("metadata", metadata);
730       }
731       if (cellRaw.output_type != null) {
732         jsonObject.addProperty("output_type", cellRaw.output_type);
733       }
734       if (cellRaw.text != null) {
735         final JsonElement text = gson.toJsonTree(cellRaw.text);
736         jsonObject.add("text", text);
737       }
738
739       return jsonObject;
740     }
741   }
742
743   static class OutputDataAdapter implements JsonSerializer<OutputDataRaw> {
744     @Override
745     public JsonElement serialize(OutputDataRaw cellRaw, Type typeOfSrc, JsonSerializationContext context) {
746       final JsonObject jsonObject = new JsonObject();
747
748       if (cellRaw.png != null) {
749         jsonObject.addProperty("image/png", cellRaw.png);
750       }
751       if (cellRaw.html != null) {
752         final JsonElement html = gson.toJsonTree(cellRaw.html);
753         jsonObject.add("text/html", html);
754       }
755       if (cellRaw.svg != null) {
756         final JsonElement svg = gson.toJsonTree(cellRaw.svg);
757         jsonObject.add("image/svg+xml", svg);
758       }
759       if (cellRaw.jpeg != null) {
760         final JsonElement jpeg = gson.toJsonTree(cellRaw.jpeg);
761         jsonObject.add("image/jpeg", jpeg);
762       }
763       if (cellRaw.latex != null) {
764         final JsonElement latex = gson.toJsonTree(cellRaw.latex);
765         jsonObject.add("text/latex", latex);
766       }
767       if (cellRaw.text != null) {
768         final JsonElement text = gson.toJsonTree(cellRaw.text);
769         jsonObject.add("text/plain", text);
770       }
771
772       return jsonObject;
773     }
774   }
775 }