Skip to content
This repository was archived by the owner on Jun 3, 2025. It is now read-only.

Commit 8700e76

Browse files
author
Irene
committed
Add analytics and refactor loading user information
Only create one tmc core instead overriting old one with new core every time new user information is loaded Change time tracker functionality to save last submit to properties
1 parent d653793 commit 8700e76

44 files changed

Lines changed: 763 additions & 243 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/fi/helsinki/cs/tmc/cli/Application.java

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package fi.helsinki.cs.tmc.cli;
22

3+
import fi.helsinki.cs.tmc.cli.analytics.AnalyticsFacade;
4+
import fi.helsinki.cs.tmc.cli.analytics.AnalyticsSettings;
5+
import fi.helsinki.cs.tmc.cli.analytics.TimeTracker;
6+
import fi.helsinki.cs.tmc.cli.backend.Settings;
7+
import fi.helsinki.cs.tmc.cli.command.SubmitCommand;
38
import fi.helsinki.cs.tmc.cli.core.AbstractCommand;
49
import fi.helsinki.cs.tmc.cli.core.CliContext;
510
import fi.helsinki.cs.tmc.cli.core.CommandFactory;
6-
import fi.helsinki.cs.tmc.cli.io.Color;
7-
import fi.helsinki.cs.tmc.cli.io.ColorUtil;
8-
import fi.helsinki.cs.tmc.cli.io.EnvironmentUtil;
9-
import fi.helsinki.cs.tmc.cli.io.HelpGenerator;
10-
import fi.helsinki.cs.tmc.cli.io.Io;
11-
import fi.helsinki.cs.tmc.cli.io.ShutdownHandler;
11+
import fi.helsinki.cs.tmc.cli.io.*;
1212
import fi.helsinki.cs.tmc.cli.updater.AutoUpdater;
1313

14+
import fi.helsinki.cs.tmc.core.TmcCore;
15+
import fi.helsinki.cs.tmc.langs.util.TaskExecutor;
16+
import fi.helsinki.cs.tmc.langs.util.TaskExecutorImpl;
17+
import fi.helsinki.cs.tmc.spyware.EventSendBuffer;
18+
import fi.helsinki.cs.tmc.spyware.EventStore;
1419
import org.apache.commons.cli.CommandLine;
1520
import org.apache.commons.cli.GnuParser;
1621
import org.apache.commons.cli.Option;
@@ -20,14 +25,7 @@
2025
import org.slf4j.Logger;
2126
import org.slf4j.LoggerFactory;
2227

23-
import java.lang.management.ManagementFactory;
24-
import java.util.List;
25-
import java.util.Map;
26-
import java.util.Set;
27-
import java.util.Scanner;
28-
import java.util.Date;
29-
import java.util.ArrayList;
30-
import java.util.Arrays;
28+
import java.util.*;
3129

3230
/**
3331
* The application class for the program.
@@ -47,7 +45,10 @@ public class Application {
4745
private String commandName;
4846
private boolean noAutoUpdate;
4947

48+
private TimeTracker timeTracker;
49+
5050
public Application(CliContext context) {
51+
this.timeTracker = new TimeTracker(context);
5152
this.parser = new GnuParser();
5253
this.options = new Options();
5354

@@ -88,11 +89,32 @@ private boolean runCommand(String name, String[] args) {
8889
io.errorln("Command " + name + " doesn't exist.");
8990
return false;
9091
}
92+
Optional<Thread> thread = sendAnalytics(command);
9193

9294
command.execute(context, args);
95+
joinNewThread(thread);
9396
return true;
9497
}
9598

99+
private Optional<Thread> sendAnalytics(AbstractCommand command) {
100+
Optional<Thread> thread = Optional.empty();
101+
if (command instanceof SubmitCommand || timeTracker.anHourHasPassedSinceLastSubmit()) {
102+
thread = this.context.getAnalyticsFacade().sendAnalytics();
103+
timeTracker.restart();
104+
}
105+
return thread;
106+
}
107+
108+
private void joinNewThread(Optional<Thread> thread) {
109+
thread.ifPresent(t -> {
110+
try {
111+
t.join();
112+
} catch (InterruptedException e) {
113+
logger.warn("Analytics thread interrupted");
114+
}
115+
});
116+
}
117+
96118
private String[] parseArgs(String[] args) {
97119
CommandLine line;
98120
try {
@@ -180,7 +202,13 @@ public void run(String[] args) {
180202

181203

182204
public static void main(String[] args) {
183-
Application app = new Application(new CliContext(null));
205+
Settings settings = new Settings();
206+
TaskExecutor tmcLangs = new TaskExecutorImpl();
207+
TmcCore core = new TmcCore(settings, tmcLangs);
208+
AnalyticsSettings analyticsSettings = new AnalyticsSettings();
209+
EventSendBuffer eventSendBuffer = new EventSendBuffer(analyticsSettings, new EventStore());
210+
AnalyticsFacade analyticsFacade = new AnalyticsFacade(analyticsSettings, eventSendBuffer);
211+
Application app = new Application(new CliContext(null, core, new WorkDir(), settings, analyticsFacade));
184212
app.run(args);
185213
}
186214

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package fi.helsinki.cs.tmc.cli.analytics;
2+
3+
import fi.helsinki.cs.tmc.core.domain.Course;
4+
import fi.helsinki.cs.tmc.core.domain.Exercise;
5+
import fi.helsinki.cs.tmc.core.exceptions.UninitializedHolderException;
6+
import fi.helsinki.cs.tmc.core.holders.TmcSettingsHolder;
7+
import fi.helsinki.cs.tmc.spyware.EventSendBuffer;
8+
import fi.helsinki.cs.tmc.spyware.EventStore;
9+
import fi.helsinki.cs.tmc.spyware.LoggableEvent;
10+
11+
import java.awt.*;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Optional;
15+
import java.util.logging.Logger;
16+
17+
public class AnalyticsFacade {
18+
private EventSendBuffer eventSendBuffer;
19+
private AnalyticsSettings settings;
20+
private static final Logger log = Logger.getLogger(AnalyticsFacade.class.getName());
21+
22+
public AnalyticsFacade(AnalyticsSettings settings, EventSendBuffer eventSendBuffer) {
23+
this.settings = settings;
24+
this.eventSendBuffer = eventSendBuffer;
25+
}
26+
27+
public void saveAnalytics(String command) {
28+
if (!settings.isSpywareEnabled()) {
29+
return;
30+
}
31+
LoggableEvent event = LoggableEventCreator.createEvent(command);
32+
this.eventSendBuffer.receiveEvent(event);
33+
}
34+
35+
public void saveAnalytics(String courseName, String command) {
36+
if (!settings.isSpywareEnabled()) {
37+
return;
38+
}
39+
LoggableEvent event = LoggableEventCreator.createEvent(courseName, command);
40+
this.eventSendBuffer.receiveEvent(event);
41+
}
42+
43+
public void saveAnalytics(Course course, String command) {
44+
if (!settings.isSpywareEnabled()) {
45+
return;
46+
}
47+
LoggableEvent event = LoggableEventCreator.createEvent(course, command);
48+
this.eventSendBuffer.receiveEvent(event);
49+
}
50+
51+
public void saveAnalytics(Exercise exercise, String command) {
52+
if (!settings.isSpywareEnabled()) {
53+
return;
54+
}
55+
LoggableEvent event = LoggableEventCreator.createEvent(exercise, command);
56+
this.eventSendBuffer.receiveEvent(event);
57+
}
58+
59+
public Optional<Thread> sendAnalytics() {
60+
if (!settings.isSpywareEnabled()) {
61+
return Optional.empty();
62+
}
63+
Thread t = new Thread(() -> this.eventSendBuffer.sendNow());
64+
t.run();
65+
return Optional.of(t);
66+
}
67+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package fi.helsinki.cs.tmc.cli.analytics;
2+
3+
import fi.helsinki.cs.tmc.spyware.SpywareSettings;
4+
5+
6+
public class AnalyticsSettings implements SpywareSettings {
7+
private boolean spyWareEnabled;
8+
private boolean detailedSpywareEnabled;
9+
10+
@Override
11+
public boolean isSpywareEnabled() {
12+
return this.spyWareEnabled;
13+
}
14+
15+
public void setSpyWareEnabled(boolean value) {
16+
this.spyWareEnabled = value;
17+
}
18+
19+
@Override
20+
public boolean isDetailedSpywareEnabled() {
21+
return false;
22+
}
23+
24+
public void setDetailedSpywareEnabled(boolean value) {
25+
this.detailedSpywareEnabled = value;
26+
}
27+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package fi.helsinki.cs.tmc.cli.analytics;
2+
3+
import com.google.gson.Gson;
4+
import fi.helsinki.cs.tmc.core.domain.Course;
5+
import fi.helsinki.cs.tmc.core.domain.Exercise;
6+
import fi.helsinki.cs.tmc.spyware.LoggableEvent;
7+
8+
import java.nio.charset.Charset;
9+
import java.util.Collections;
10+
11+
public class LoggableEventCreator {
12+
13+
public static LoggableEvent createEvent(String command) {
14+
byte[] jsonBytes = getBytes(command);
15+
return new LoggableEvent("project_action", jsonBytes);
16+
}
17+
18+
public static LoggableEvent createEvent(String courseName, String command) {
19+
byte[] jsonBytes = getBytes(command);
20+
return new LoggableEvent(courseName, "", "project_action", jsonBytes);
21+
}
22+
23+
public static LoggableEvent createEvent(Course course, String command) {
24+
byte[] jsonBytes = getBytes(command);
25+
return new LoggableEvent(course, "project_action", jsonBytes);
26+
}
27+
28+
public static LoggableEvent createEvent(Exercise exercise, String command) {
29+
byte[] jsonBytes = getBytes(command);
30+
return new LoggableEvent(exercise, "project_action", jsonBytes);
31+
}
32+
33+
private static byte[] getBytes(String command) {
34+
Object data = Collections.singletonMap("command", command);
35+
String json = new Gson().toJson(data);
36+
return json.getBytes(Charset.forName("UTF-8"));
37+
}
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package fi.helsinki.cs.tmc.cli.analytics;
2+
import fi.helsinki.cs.tmc.cli.backend.Settings;
3+
import fi.helsinki.cs.tmc.cli.backend.SettingsIo;
4+
import fi.helsinki.cs.tmc.cli.core.CliContext;
5+
6+
import java.util.HashMap;
7+
import java.util.concurrent.TimeUnit;
8+
9+
public class TimeTracker {
10+
private HashMap<String, String> properties;
11+
private static final String PROPERTY_KEY = "last-submit";
12+
13+
public TimeTracker(CliContext context) {
14+
this.properties = context.getProperties();
15+
}
16+
17+
public void restart() {
18+
long startTime = System.nanoTime();
19+
properties.put(PROPERTY_KEY, Long.toString(startTime));
20+
SettingsIo.saveProperties(properties);
21+
}
22+
23+
public boolean anHourHasPassedSinceLastSubmit() {
24+
long startTime = Long.parseLong(SettingsIo.loadProperties().get(PROPERTY_KEY));
25+
long endTime = System.nanoTime();
26+
long elapsedTime = endTime - startTime;
27+
long hoursPassed = TimeUnit.HOURS.convert(elapsedTime, TimeUnit.NANOSECONDS);
28+
return hoursPassed >= 1;
29+
}
30+
31+
}

src/main/java/fi/helsinki/cs/tmc/cli/backend/SettingsIo.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class SettingsIo {
2929
public static final String CONFIG_DIR = "tmc-cli";
3030

3131
// ACCOUNTS_CONFIG is the _global_ configuration file containing all
32-
// user login information including usernames, passwords (in plain text)
32+
// user login information including usernames, oauth credentials
3333
// and servers. Is located under CONFIG_DIR
3434
public static final String ACCOUNTS_CONFIG = "accounts.json";
3535

@@ -114,7 +114,7 @@ public static boolean savePropertiesTo(HashMap<String, String> properties, Path
114114
* Get the correct directory in which our config files go
115115
* ie /home/user/.config/tmc-cli/.
116116
*/
117-
static Path getConfigDirectory() {
117+
public static Path getConfigDirectory() {
118118
Path configPath;
119119
if (EnvironmentUtil.isWindows()) {
120120
String appdata = System.getenv("APPDATA");
@@ -144,7 +144,7 @@ private static Path getAccountsFile(Path configRoot) {
144144
return file;
145145
}
146146

147-
private static Path getPropertiesFile(Path configRoot) {
147+
public static Path getPropertiesFile(Path configRoot) {
148148
Path file = configRoot.resolve(PROPERTIES_CONFIG);
149149
if (!requireConfigDirectory(configRoot)) {
150150
return null;

src/main/java/fi/helsinki/cs/tmc/cli/command/ConfigCommand.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import fi.helsinki.cs.tmc.cli.utils.BadValueTypeException;
1010
import fi.helsinki.cs.tmc.cli.utils.BiConsumerWithException;
11-
import fi.helsinki.cs.tmc.core.exceptions.NotLoggedInException;
11+
import fi.helsinki.cs.tmc.core.domain.ProgressObserver;
12+
import fi.helsinki.cs.tmc.core.holders.TmcSettingsHolder;
13+
import fi.helsinki.cs.tmc.core.utilities.TmcServerAddressNormalizer;
1214
import org.apache.commons.cli.CommandLine;
1315
import org.apache.commons.cli.Options;
1416

@@ -56,6 +58,10 @@ private void configureAllowedKeys() {
5658
throw new BadValueTypeException("Please start the address with http[s]://");
5759
}
5860
context.getSettings().setServerAddress(addr);
61+
if (!normalizeServerAddress()) {
62+
io.println("There was a problem setting the server address.");
63+
return;
64+
}
5965
SettingsIo.saveCurrentSettingsToAccountList(context.getSettings());
6066
});
6167

@@ -103,6 +109,12 @@ public void run(CliContext context, CommandLine args) {
103109
arguments = Arrays.stream(arguments).filter(o -> !o.trim().isEmpty()).toArray(String[]::new);
104110
this.properties = context.getProperties();
105111

112+
if (this.context.getSettings().getUsername().isPresent()) {
113+
this.context.getAnalyticsFacade().saveAnalytics(this.context.getSettings().getUsername().get(), "config");
114+
} else {
115+
this.context.getAnalyticsFacade().saveAnalytics("config");
116+
}
117+
106118
if ((get ? 1 : 0) + (listing ? 1 : 0) + (delete ? 1 : 0) > 1) {
107119
io.errorln("Only one of the --get or --list or --delete options can "
108120
+ "be used at same time.");
@@ -258,4 +270,16 @@ private void addToProperties(String key, Object value) throws BadValueTypeExcept
258270
properties.put(key, color);
259271
SettingsIo.saveProperties(properties);
260272
}
273+
274+
private boolean normalizeServerAddress() {
275+
TmcServerAddressNormalizer normalizer = new TmcServerAddressNormalizer();
276+
normalizer.normalize();
277+
try {
278+
this.context.getTmcCore().authenticate(ProgressObserver.NULL_OBSERVER, TmcSettingsHolder.get().getPassword().get()).call();
279+
} catch (Exception e) {
280+
return false;
281+
}
282+
normalizer.selectOrganizationAndCourse();
283+
return true;
284+
}
261285
}

src/main/java/fi/helsinki/cs/tmc/cli/command/DownloadExercisesCommand.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public void run(CliContext context, CommandLine args) {
5959
ctx = context;
6060
showAll = args.hasOption("a");
6161

62-
if (!ctx.loadBackend()) {
62+
if (!ctx.checkIsLoggedIn()) {
6363
return;
6464
}
6565

@@ -75,6 +75,9 @@ public void run(CliContext context, CommandLine args) {
7575
return;
7676
}
7777
Course course = finder.getCourse();
78+
79+
this.ctx.getAnalyticsFacade().saveAnalytics(course, "download_exercises");
80+
7881
List<Exercise> filtered = getFilteredExercises(course);
7982
// todo: If -c switch, use core.downloadCompletedExercises() to download user's old
8083
// submissions. Not yet implemented in tmc-core.
@@ -93,7 +96,6 @@ public void run(CliContext context, CommandLine args) {
9396
}
9497

9598
printStatistics(course, filtered.size(), exercises.size());
96-
9799
}
98100

99101
private List<Exercise> getFilteredExercises(Course course) {

src/main/java/fi/helsinki/cs/tmc/cli/command/HelpCommand.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package fi.helsinki.cs.tmc.cli.command;
22

3-
import fi.helsinki.cs.tmc.cli.Application;
43
import fi.helsinki.cs.tmc.cli.core.AbstractCommand;
54
import fi.helsinki.cs.tmc.cli.core.CliContext;
65
import fi.helsinki.cs.tmc.cli.core.Command;
@@ -35,6 +34,10 @@ public void run(CliContext context, CommandLine args) {
3534
this.context = context;
3635
this.io = context.getIo();
3736

37+
if (this.context.checkIsLoggedIn()) {
38+
this.context.getAnalyticsFacade().saveAnalytics(this.context.getSettings().getUsername().get(), "help");
39+
}
40+
3841
String category = handleArgs(args);
3942
if (category == null) {
4043
return;

0 commit comments

Comments
 (0)