Skip to content

Commit 4634462

Browse files
committed
Merge pull request #128 from testmycode/output
Collect interaction with output tab for tmc projects.
2 parents ea2a2ad + 7ed2417 commit 4634462

File tree

5 files changed

+374
-10
lines changed

5 files changed

+374
-10
lines changed

tmc-plugin/src/fi/helsinki/cs/tmc/spyware/SpywareFacade.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import fi.helsinki.cs.tmc.model.CourseDb;
77
import fi.helsinki.cs.tmc.model.ServerAccess;
88
import fi.helsinki.cs.tmc.model.TmcSettings;
9+
import fi.helsinki.cs.tmc.spyware.eventsources.OutputActionCaptor;
10+
import fi.helsinki.cs.tmc.spyware.eventsources.OutputActionEventSource;
911
import fi.helsinki.cs.tmc.spyware.eventsources.TextInsertEventSource;
1012
import fi.helsinki.cs.tmc.spyware.eventsources.ProjectActionCaptor;
1113
import fi.helsinki.cs.tmc.spyware.eventsources.ProjectActionEventSource;
@@ -63,6 +65,7 @@ public void run() {
6365

6466
private SourceSnapshotEventSource sourceSnapshotSource;
6567
private ProjectActionEventSource projectActionSource;
68+
private OutputActionEventSource outputActionSource;
6669
private TmcEventBusEventSource tmcEventBusSource;
6770
private TextInsertEventSource textInsertEventSource;
6871
private WindowStatechangesEventSource windowStatechangesEventSource;
@@ -104,6 +107,7 @@ public SpywareFacade() {
104107

105108
projectActionSource = new ProjectActionEventSource(taggingSender);
106109
tmcEventBusSource = new TmcEventBusEventSource(taggingSender);
110+
outputActionSource = new OutputActionEventSource(taggingSender);
107111

108112
windowStatechangesEventSource = new WindowStatechangesEventSource(taggingSender);
109113
TmcSwingUtilities.ensureEdt(new Runnable() {
@@ -112,6 +116,7 @@ public void run() {
112116
ProjectActionCaptor.addListener(projectActionSource);
113117
TmcEventBus.getDefault().subscribeStrongly(tmcEventBusSource);
114118
textInsertEventSource = new TextInsertEventSource(taggingSender);
119+
OutputActionCaptor.addListener(outputActionSource);
115120
}
116121
});
117122
}
@@ -126,6 +131,7 @@ public void run() {
126131
textInsertEventSource.close();
127132
TmcEventBus.getDefault().unsubscribe(tmcEventBusSource);
128133
ProjectActionCaptor.removeListener(projectActionSource);
134+
OutputActionCaptor.removeListener(outputActionSource);
129135
}
130136
});
131137

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package fi.helsinki.cs.tmc.spyware.eventsources;
2+
3+
import fi.helsinki.cs.tmc.data.Exercise;
4+
import fi.helsinki.cs.tmc.model.CourseDb;
5+
import fi.helsinki.cs.tmc.model.ProjectMediator;
6+
import fi.helsinki.cs.tmc.model.TmcProjectInfo;
7+
8+
import org.netbeans.api.annotations.common.NullAllowed;
9+
import org.netbeans.api.editor.EditorRegistry;
10+
import org.netbeans.modules.editor.NbEditorDocument;
11+
import org.openide.filesystems.FileObject;
12+
import org.openide.loaders.DataObject;
13+
import org.openide.util.Lookup;
14+
import org.openide.windows.IOProvider;
15+
import org.openide.windows.InputOutput;
16+
import org.openide.windows.OutputWriter;
17+
18+
import java.io.IOException;
19+
import java.io.Reader;
20+
import java.nio.CharBuffer;
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
import java.util.List;
24+
import java.util.logging.Level;
25+
import java.util.logging.Logger;
26+
import javax.swing.text.Document;
27+
import javax.swing.text.JTextComponent;
28+
29+
/**
30+
* Provides tmc-spyware access to Output of netbeans and allows us to sniff all
31+
* keypresses read by app with it.
32+
*/
33+
@org.openide.util.lookup.ServiceProvider(service = org.openide.windows.IOProvider.class, position = -9999999)
34+
public class OutputActionCaptor extends IOProvider {
35+
36+
private static final Logger log = Logger.getLogger(OutputActionCaptor.class.getName());
37+
private static final String NAME = "tmc-output-logger";
38+
39+
private static final CourseDb courseDb = CourseDb.getInstance();
40+
41+
public static interface Listener {
42+
43+
public void input(Exercise exercise, char character);
44+
public void input(Exercise exercise, char[] characters);
45+
public void input(Exercise exercise, CharBuffer characters);
46+
}
47+
48+
private static final List<Listener> listeners = new ArrayList<Listener>();
49+
50+
public synchronized static void addListener(Listener listener) {
51+
listeners.add(listener);
52+
}
53+
54+
public synchronized static void removeListener(Listener listener) {
55+
listeners.remove(listener);
56+
}
57+
58+
@Override
59+
public InputOutput getIO(String string, boolean bln) {
60+
for (IOProvider provider : Lookup.getDefault().lookupAll(IOProvider.class)) {
61+
if (provider.getName().equals(NAME)) {
62+
continue;
63+
}
64+
65+
InputOutput inputOutput = provider.getIO(string, bln);
66+
return new TmcInputOutputProxy(inputOutput);
67+
}
68+
log.log(Level.WARNING, "Failed to get IO. Please contact TestMyCode authors/teacher");
69+
throw new RuntimeException("Failed to get IO. Please contact TestMyCode authors/teacher");
70+
}
71+
72+
/**
73+
* Proxies access to InputOutput from the default provider. Used as one
74+
* event source for spyware module.
75+
*/
76+
private static final class TmcInputOutputProxy implements InputOutput {
77+
78+
private final InputOutput original;
79+
private final Exercise exercise;
80+
81+
public TmcInputOutputProxy(InputOutput original) {
82+
this.original = original;
83+
this.exercise = getExercise(getChangedFile());
84+
}
85+
86+
@Override
87+
public OutputWriter getOut() {
88+
return original.getOut();
89+
}
90+
91+
@Override
92+
public Reader getIn() {
93+
final Reader reader = original.getIn();
94+
return new TmcReader(reader);
95+
}
96+
97+
/**
98+
* Proxies access to Reader, allowing us to sniff the interactions.
99+
*/
100+
private final class TmcReader extends Reader {
101+
102+
private final Reader original;
103+
104+
public TmcReader(Reader original) {
105+
this.original = original;
106+
}
107+
108+
@Override
109+
public int read(char[] cbuf, int off, int len) throws IOException {
110+
return original.read(cbuf, off, len);
111+
}
112+
113+
private void emit(char c) {
114+
for (Listener listener : listeners) {
115+
listener.input(exercise, c);
116+
}
117+
}
118+
private void emit(CharBuffer charBuf) {
119+
for (Listener listener : listeners) {
120+
listener.input(exercise, charBuf);
121+
}
122+
}
123+
124+
private void emit(char[] charAry) {
125+
for (Listener listener : listeners) {
126+
listener.input(exercise, charAry);
127+
}
128+
}
129+
130+
@Override
131+
public int read() throws IOException {
132+
int c = original.read();
133+
emit((char) c);
134+
return c;
135+
}
136+
137+
@Override
138+
public int read(CharBuffer target) throws IOException {
139+
int i = original.read(target);
140+
emit(target.duplicate());
141+
return i;
142+
}
143+
144+
@Override
145+
public int read(char[] cbuf) throws IOException {
146+
int i = original.read(cbuf);
147+
emit(Arrays.copyOf(cbuf, cbuf.length));
148+
return i;
149+
}
150+
151+
@Override
152+
public boolean ready() throws IOException {
153+
return original.ready();
154+
}
155+
156+
@Override
157+
public void reset() throws IOException {
158+
original.reset();
159+
}
160+
161+
@Override
162+
public long skip(long n) throws IOException {
163+
return original.skip(n);
164+
}
165+
166+
@Override
167+
public void close() throws IOException {
168+
original.close();
169+
}
170+
171+
@Override
172+
public void mark(int readAheadLimit) throws IOException {
173+
original.mark(readAheadLimit);
174+
}
175+
176+
@Override
177+
public boolean markSupported() {
178+
return original.markSupported();
179+
}
180+
}
181+
182+
@Override
183+
public OutputWriter getErr() {
184+
return original.getErr();
185+
}
186+
187+
@Override
188+
public void closeInputOutput() {
189+
original.closeInputOutput();
190+
}
191+
192+
@Override
193+
public boolean isClosed() {
194+
return original.isClosed();
195+
}
196+
197+
@Override
198+
public void setOutputVisible(boolean bln) {
199+
original.setOutputVisible(bln);
200+
}
201+
202+
@Override
203+
public void setErrVisible(boolean bln) {
204+
original.setErrVisible(bln);
205+
}
206+
207+
@Override
208+
public void setInputVisible(boolean bln) {
209+
original.setInputVisible(bln);
210+
}
211+
212+
@Override
213+
public void select() {
214+
original.select();
215+
}
216+
217+
@Override
218+
public boolean isErrSeparated() {
219+
return original.isErrSeparated();
220+
}
221+
222+
@Override
223+
public void setErrSeparated(boolean bln) {
224+
original.setErrSeparated(bln);
225+
}
226+
227+
@Override
228+
public boolean isFocusTaken() {
229+
return original.isFocusTaken();
230+
}
231+
232+
@Override
233+
public void setFocusTaken(boolean bln) {
234+
original.setFocusTaken(bln);
235+
}
236+
237+
@Override
238+
@Deprecated
239+
public Reader flushReader() {
240+
return original.flushReader();
241+
}
242+
243+
private Exercise getExercise(@NullAllowed FileObject obj) {
244+
if (obj == null) {
245+
return null;
246+
}
247+
ProjectMediator pm = ProjectMediator.getInstance();
248+
TmcProjectInfo project = pm.tryGetProjectOwningFile(obj);
249+
return pm.tryGetExerciseForProject(project, courseDb);
250+
}
251+
252+
/**
253+
* Returns {@link FileObject} representing the last active file for each
254+
* event.
255+
*/
256+
private FileObject getChangedFile() {
257+
JTextComponent jtc = EditorRegistry.lastFocusedComponent();
258+
if (jtc != null) {
259+
Document d = jtc.getDocument();
260+
Object fileObj = d.getProperty(NbEditorDocument.StreamDescriptionProperty);
261+
if (fileObj instanceof DataObject) {
262+
DataObject changedDataObject = (DataObject) fileObj;
263+
return changedDataObject.getPrimaryFile();
264+
}
265+
}
266+
return null;
267+
}
268+
}
269+
270+
@Override
271+
public OutputWriter getStdOut() {
272+
for (IOProvider provider : Lookup.getDefault().lookupAll(IOProvider.class)) {
273+
if (provider.getName().equals(NAME)) {
274+
continue;
275+
}
276+
return provider.getStdOut();
277+
278+
}
279+
throw new RuntimeException("Failed to get StdOut. Please contact TestMyCode authors/teacher");
280+
}
281+
282+
@Override
283+
public String getName() {
284+
return NAME;
285+
}
286+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package fi.helsinki.cs.tmc.spyware.eventsources;
2+
3+
import com.google.gson.Gson;
4+
import fi.helsinki.cs.tmc.data.Exercise;
5+
import fi.helsinki.cs.tmc.model.CourseDb;
6+
import fi.helsinki.cs.tmc.model.ProjectMediator;
7+
import fi.helsinki.cs.tmc.spyware.EventReceiver;
8+
import fi.helsinki.cs.tmc.spyware.LoggableEvent;
9+
import java.nio.CharBuffer;
10+
import java.nio.charset.Charset;
11+
import java.util.Arrays;
12+
import java.util.Collections;
13+
import java.util.logging.Level;
14+
import java.util.logging.Logger;
15+
16+
public class OutputActionEventSource implements OutputActionCaptor.Listener {
17+
18+
private static final Logger log = Logger.getLogger(OutputActionEventSource.class.getName());
19+
20+
private final ProjectMediator projects;
21+
private final CourseDb courseDb;
22+
private final EventReceiver receiver;
23+
24+
public OutputActionEventSource(EventReceiver receiver) {
25+
this.projects = ProjectMediator.getInstance();
26+
this.courseDb = CourseDb.getInstance();
27+
this.receiver = receiver;
28+
}
29+
30+
@Override
31+
public void input(Exercise exercise, char character) {
32+
if (exercise != null) {
33+
Object data = Collections.singletonMap("char", character);
34+
String jsonData = new Gson().toJson(data);
35+
LoggableEvent event = new LoggableEvent(exercise, "output_action", jsonData.getBytes(Charset.forName("UTF-8")));
36+
receiver.receiveEvent(event);
37+
log.log(Level.FINE, "Output action event {0} for {1}", new Object[]{
38+
character,
39+
exercise.getName()
40+
});
41+
}
42+
}
43+
44+
@Override
45+
public void input(Exercise exercise, char[] characters) {
46+
if (exercise != null) {
47+
Object data = Collections.singletonMap("char", characters);
48+
String jsonData = new Gson().toJson(data);
49+
LoggableEvent event = new LoggableEvent(exercise, "output_action", jsonData.getBytes(Charset.forName("UTF-8")));
50+
receiver.receiveEvent(event);
51+
log.log(Level.FINE, "Output action event {0} for {1}", new Object[]{
52+
Arrays.toString(characters),
53+
exercise.getName()
54+
});
55+
}
56+
}
57+
58+
@Override
59+
public void input(Exercise exercise, CharBuffer characters) {
60+
if (exercise != null) {
61+
Object data = Collections.singletonMap("char", characters);
62+
String jsonData = new Gson().toJson(data);
63+
LoggableEvent event = new LoggableEvent(exercise, "output_action", jsonData.getBytes(Charset.forName("UTF-8")));
64+
receiver.receiveEvent(event);
65+
log.log(Level.FINE, "Output action event {0} for {1}", new Object[]{
66+
characters,
67+
exercise.getName()
68+
});
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)