Skip to content

Commit bbe8abe

Browse files
Java upload tab (#2945)
* add class file upload tab * unused code param * fix imports * update reducer pattern * update upload text * fix: tsc missing properties * disable upload tab on prod * fix comments * Fix component typing * Fix incorrect workspace location in reducer * Refactor SideContentUpload component * Memoize change handler * Fix layout and UI bugs * Use Blueprint file input instead of HTML input component * Fix typing * remove location prop * Show upload count * Fix format * Improve typing * Update typing --------- Co-authored-by: Richard Dominick <[email protected]>
1 parent 2d108b9 commit bbe8abe

File tree

12 files changed

+202
-32
lines changed

12 files changed

+202
-32
lines changed

src/commons/application/ApplicationTypes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,8 @@ export const createDefaultWorkspace = (workspaceLocation: WorkspaceLocation): Wo
419419
enableDebugging: true,
420420
debuggerContext: {} as DebuggerContext,
421421
lastDebuggerResult: undefined,
422-
lastNonDetResult: null
422+
lastNonDetResult: null,
423+
files: {}
423424
});
424425

425426
const defaultFileName = 'program.js';
@@ -447,6 +448,7 @@ export const defaultWorkspaceManager: WorkspaceManagerState = {
447448
usingSubst: false,
448449
usingCse: false,
449450
updateCse: true,
451+
usingUpload: false,
450452
currentStep: -1,
451453
stepsTotal: 0,
452454
breakpointSteps: [],
@@ -501,6 +503,7 @@ export const defaultWorkspaceManager: WorkspaceManagerState = {
501503
usingSubst: false,
502504
usingCse: false,
503505
updateCse: true,
506+
usingUpload: false,
504507
currentStep: -1,
505508
stepsTotal: 0,
506509
breakpointSteps: [],

src/commons/sagas/PlaygroundSaga.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
toggleUpdateCse,
2727
toggleUsingCse,
2828
toggleUsingSubst,
29+
toggleUsingUpload,
2930
updateCurrentStep,
3031
updateStepsTotal
3132
} from '../workspace/WorkspaceActions';
@@ -123,6 +124,12 @@ export default function* PlaygroundSaga(): SagaIterator {
123124
}
124125
}
125126

127+
if (newId === SideContentType.upload) {
128+
yield put(toggleUsingUpload(true, workspaceLocation));
129+
} else {
130+
yield put(toggleUsingUpload(false, workspaceLocation));
131+
}
132+
126133
if (isSchemeLanguage(playgroundSourceChapter) && newId === SideContentType.cseMachine) {
127134
yield put(toggleUsingCse(true, workspaceLocation));
128135
}

src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ export function* evalCodeSaga(
6464
context.executionMethod = 'interpreter';
6565
}
6666

67+
const uploadIsActive: boolean = correctWorkspace
68+
? yield select(
69+
(state: OverallState) =>
70+
(state.workspaces[workspaceLocation] as PlaygroundWorkspaceState | SicpWorkspaceState)
71+
.usingUpload
72+
)
73+
: false;
74+
const uploads = yield select((state: OverallState) => state.workspaces[workspaceLocation].files);
75+
6776
// For the CSE machine slider
6877
const cseIsActive: boolean = correctWorkspace
6978
? yield select(
@@ -262,7 +271,10 @@ export function* evalCodeSaga(
262271
: isC
263272
? call(cCompileAndRun, entrypointCode, context)
264273
: isJava
265-
? call(javaRun, entrypointCode, context, currentStep, isUsingCse)
274+
? call(javaRun, entrypointCode, context, currentStep, isUsingCse, {
275+
uploadIsActive,
276+
uploads
277+
})
266278
: call(
267279
runFilesInContext,
268280
isFolderModeEnabled

src/commons/sagas/__tests__/PlaygroundSaga.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ describe('Playground saga tests', () => {
8585
usingSubst: false,
8686
usingCse: false,
8787
updateCse: true,
88+
usingUpload: false,
8889
currentStep: -1,
8990
stepsTotal: 0,
9091
breakpointSteps: [],
@@ -153,6 +154,7 @@ describe('Playground saga tests', () => {
153154
usingSubst: false,
154155
usingCse: false,
155156
updateCse: true,
157+
usingUpload: false,
156158
currentStep: -1,
157159
stepsTotal: 0,
158160
breakpointSteps: [],
@@ -221,6 +223,7 @@ describe('Playground saga tests', () => {
221223
usingSubst: false,
222224
usingCse: false,
223225
updateCse: true,
226+
usingUpload: false,
224227
currentStep: -1,
225228
stepsTotal: 0,
226229
breakpointSteps: [],
@@ -271,6 +274,7 @@ describe('Playground saga tests', () => {
271274
],
272275
usingSubst: false,
273276
usingCse: false,
277+
usingUpload: false,
274278
updateCse: true,
275279
currentStep: -1,
276280
stepsTotal: 0,
@@ -342,6 +346,7 @@ describe('Playground saga tests', () => {
342346
usingSubst: false,
343347
usingCse: false,
344348
updateCse: true,
349+
usingUpload: false,
345350
currentStep: -1,
346351
stepsTotal: 0,
347352
breakpointSteps: [],
@@ -401,6 +406,7 @@ describe('Playground saga tests', () => {
401406
usingSubst: false,
402407
usingCse: false,
403408
updateCse: true,
409+
usingUpload: false,
404410
currentStep: -1,
405411
stepsTotal: 0,
406412
breakpointSteps: [],

src/commons/sideContent/SideContentTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export enum SideContentType {
4141
testcases = 'testcases',
4242
toneMatrix = 'tone_matrix',
4343
htmlDisplay = 'html_display',
44-
storiesRun = 'stories_run'
44+
storiesRun = 'stories_run',
45+
upload = 'upload'
4546
}
4647

4748
/**
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { FileInput } from '@blueprintjs/core';
2+
import { IconNames } from '@blueprintjs/icons';
3+
import React, { useCallback } from 'react';
4+
5+
import { SideContentTab, SideContentType } from '../SideContentTypes';
6+
7+
export type UploadResult = {
8+
[key: string]: any;
9+
};
10+
11+
async function getBase64(file: Blob, onFinish: (result: string) => void) {
12+
const reader = new FileReader();
13+
return new Promise((resolve, reject) => {
14+
reader.readAsDataURL(file);
15+
reader.onload = () => {
16+
onFinish((reader.result as string).slice(37));
17+
resolve(reader.result);
18+
};
19+
reader.onerror = error => reject(error);
20+
});
21+
}
22+
23+
type Props = {
24+
onUpload: (files: UploadResult) => void;
25+
};
26+
27+
/**
28+
* This component is responsible for uploading Java class files to bypass the compiler.
29+
*/
30+
const SideContentUpload: React.FC<Props> = ({ onUpload }) => {
31+
const [count, setCount] = React.useState(0);
32+
33+
const handleFileUpload: React.ChangeEventHandler<HTMLInputElement> = useCallback(
34+
e => {
35+
const ret: { [key: string]: string } = {};
36+
const promises = [];
37+
for (const file of e.target.files ?? []) {
38+
if (file.name.endsWith('.class')) {
39+
promises.push(
40+
getBase64(file, (b64: string) => {
41+
ret[file.name] = b64;
42+
})
43+
);
44+
}
45+
}
46+
Promise.all(promises).then(() => {
47+
onUpload(ret);
48+
setCount(promises.length);
49+
});
50+
},
51+
[onUpload]
52+
);
53+
54+
return (
55+
<div>
56+
<p>Bypass the compiler and type checker by uploading class files to run in the JVM.</p>
57+
<p>
58+
Only .class files are accepted. Code in the editor will be ignored when running while this
59+
tab is active.
60+
</p>
61+
<p>Compile the files with the following command:</p>
62+
<pre>
63+
<code>javac *.java -target 8 -source 8</code>
64+
</pre>
65+
<p>Avoid running class files downloaded from unknown sources.</p>
66+
<p>
67+
<strong>Main class must be named Main and uploaded as Main.class.</strong>
68+
</p>
69+
<FileInput
70+
inputProps={{ multiple: true, accept: '.class' }}
71+
onInputChange={handleFileUpload}
72+
text={count === 0 ? 'Choose files...' : `${count} file(s) uploaded.`}
73+
/>
74+
</div>
75+
);
76+
};
77+
78+
const makeUploadTabFrom = (onUpload: (files: UploadResult) => void): SideContentTab => ({
79+
label: 'Upload files',
80+
iconName: IconNames.Upload,
81+
body: <SideContentUpload onUpload={onUpload} />,
82+
id: SideContentType.upload
83+
});
84+
85+
export default makeUploadTabFrom;

src/commons/utils/JavaHelper.ts

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@ import { compileFromSource, ECE, typeCheck } from 'java-slang';
22
import { BinaryWriter } from 'java-slang/dist/compiler/binary-writer';
33
import setupJVM, { parseBin } from 'java-slang/dist/jvm';
44
import { createModuleProxy, loadCachedFiles } from 'java-slang/dist/jvm/utils/integration';
5-
import { Context } from 'js-slang';
5+
import { Context, Result } from 'js-slang';
66
import loadSourceModules from 'js-slang/dist/modules/loader';
7-
import { ErrorSeverity, ErrorType, Result, SourceError } from 'js-slang/dist/types';
7+
import { ErrorSeverity, ErrorType, SourceError } from 'js-slang/dist/types';
88

99
import { CseMachine } from '../../features/cseMachine/java/CseMachine';
10+
import { UploadResult } from '../sideContent/content/SideContentUpload';
1011
import Constants from './Constants';
1112
import DisplayBufferService from './DisplayBufferService';
1213

1314
export async function javaRun(
1415
javaCode: string,
1516
context: Context,
1617
targetStep: number,
17-
isUsingCse: boolean
18+
isUsingCse: boolean,
19+
options?: { uploadIsActive?: boolean; uploads?: UploadResult }
1820
) {
1921
let compiled = {};
2022

@@ -28,30 +30,11 @@ export async function javaRun(
2830
});
2931
};
3032

31-
const typeCheckResult = typeCheck(javaCode);
32-
if (typeCheckResult.hasTypeErrors) {
33-
const typeErrMsg = typeCheckResult.errorMsgs.join('\n');
34-
stderr('TypeCheck', typeErrMsg);
35-
return Promise.resolve({ status: 'error' });
36-
}
37-
38-
if (isUsingCse) return await runJavaCseMachine(javaCode, targetStep, context);
39-
40-
try {
41-
const classFile = compileFromSource(javaCode);
42-
compiled = {
43-
'Main.class': Buffer.from(new BinaryWriter().generateBinary(classFile)).toString('base64')
44-
};
45-
} catch (e) {
46-
stderr('Compile', e);
47-
return Promise.resolve({ status: 'error' });
48-
}
49-
50-
let files = {};
33+
let files: UploadResult = {};
5134
let buffer: string[] = [];
5235

5336
const readClassFiles = (path: string) => {
54-
let item = files[path as keyof typeof files];
37+
let item = files[path];
5538

5639
// not found: attempt to fetch from CDN
5740
if (!item && path) {
@@ -69,11 +52,11 @@ export async function javaRun(
6952
// we might want to cache the files in IndexedDB here
7053
files = { ...files, ...json };
7154

72-
if (!files[path as keyof typeof files]) {
55+
if (!files[path]) {
7356
throw new Error('File not found: ' + path);
7457
}
7558

76-
item = files[path as keyof typeof files];
59+
item = files[path];
7760
}
7861

7962
// convert base64 to classfile object
@@ -108,6 +91,35 @@ export async function javaRun(
10891
}
10992
};
11093

94+
if (options?.uploadIsActive) {
95+
compiled = options.uploads ?? {};
96+
if (!options.uploads) {
97+
stderr('Compile', 'No files uploaded');
98+
return Promise.resolve({ status: 'error' });
99+
}
100+
} else {
101+
const typeCheckResult = typeCheck(javaCode);
102+
if (typeCheckResult.hasTypeErrors) {
103+
const typeErrMsg = typeCheckResult.errorMsgs.join('\n');
104+
stderr('TypeCheck', typeErrMsg);
105+
return Promise.resolve({ status: 'error' });
106+
}
107+
108+
if (isUsingCse) {
109+
return await runJavaCseMachine(javaCode, targetStep, context);
110+
}
111+
112+
try {
113+
const classFile = compileFromSource(javaCode);
114+
compiled = {
115+
'Main.class': Buffer.from(new BinaryWriter().generateBinary(classFile)).toString('base64')
116+
};
117+
} catch (e) {
118+
stderr('Compile', e);
119+
return Promise.resolve({ status: 'error' });
120+
}
121+
}
122+
111123
// load cached classfiles from IndexedDB
112124
return loadCachedFiles(() =>
113125
// Initial loader to fetch commonly used classfiles
@@ -195,6 +207,7 @@ export async function runJavaCseMachine(code: string, targetStep: number, contex
195207
})
196208
.catch(e => {
197209
console.error(e);
198-
return { status: 'error' } as Result;
210+
const errorResult: Result = { status: 'error' };
211+
return errorResult;
199212
});
200213
}

src/commons/workspace/WorkspaceActions.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import { ExternalLibraryName } from '../application/types/ExternalTypes';
77
import { Library } from '../assessment/AssessmentTypes';
88
import { HighlightedLines, Position } from '../editor/EditorTypes';
99
import { createActions } from '../redux/utils';
10+
import { UploadResult } from '../sideContent/content/SideContentUpload';
1011
import {
1112
EditorTabState,
1213
SubmissionsTableFilters,
14+
TOGGLE_USING_UPLOAD,
1315
UPDATE_LAST_DEBUGGER_RESULT,
1416
UPDATE_LAST_NON_DET_RESULT,
17+
UPLOAD_FILES,
1518
WorkspaceLocation,
1619
WorkspaceLocationsWithTools,
1720
WorkspaceState
@@ -278,6 +281,20 @@ export const updateLastNonDetResult = createAction(
278281
})
279282
);
280283

284+
export const toggleUsingUpload = createAction(
285+
TOGGLE_USING_UPLOAD,
286+
(usingUpload: boolean, workspaceLocation: WorkspaceLocationsWithTools) => ({
287+
payload: { usingUpload, workspaceLocation }
288+
})
289+
);
290+
291+
export const uploadFiles = createAction(
292+
UPLOAD_FILES,
293+
(files: UploadResult, workspaceLocation: WorkspaceLocation) => ({
294+
payload: { files, workspaceLocation }
295+
})
296+
);
297+
281298
// For compatibility with existing code (reducer)
282299
export const {
283300
setTokenCount,
@@ -345,5 +362,7 @@ export const {
345362
export default {
346363
...newActions,
347364
updateLastDebuggerResult,
348-
updateLastNonDetResult
365+
updateLastNonDetResult,
366+
toggleUsingUpload,
367+
uploadFiles
349368
};

src/commons/workspace/WorkspaceReducer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,18 @@ const newWorkspaceReducer = createReducer(defaultWorkspaceManager, builder => {
354354
// debuggerContext.context = action.payload.context;
355355
// debuggerContext.workspaceLocation = action.payload.workspaceLocation;
356356
// })
357+
.addCase(WorkspaceActions.toggleUsingUpload, (state, action) => {
358+
const { workspaceLocation } = action.payload;
359+
if (workspaceLocation === 'playground' || workspaceLocation === 'sicp') {
360+
state[workspaceLocation].usingUpload = action.payload.usingUpload;
361+
}
362+
})
363+
.addCase(WorkspaceActions.uploadFiles, (state, action) => {
364+
const workspaceLocation = getWorkspaceLocation(action);
365+
if (workspaceLocation === 'playground' || workspaceLocation === 'sicp') {
366+
state[workspaceLocation].files = action.payload.files;
367+
}
368+
})
357369
.addCase(WorkspaceActions.updateLastDebuggerResult, (state, action) => {
358370
const workspaceLocation = getWorkspaceLocation(action);
359371
state[workspaceLocation].lastDebuggerResult = action.payload.lastDebuggerResult;

0 commit comments

Comments
 (0)