code cleanup

This commit is contained in:
anhefti 2019-02-27 12:44:03 +01:00
parent cd8ba371cc
commit 1e7b6f807f
23 changed files with 211 additions and 207 deletions

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.HttpURLConnection;
@ -22,6 +23,8 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.context.annotation.Bean;
@ -57,6 +60,8 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@Order(6)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ErrorController {
private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
@Value("${sebserver.webservice.api.admin.endpoint}")
private String adminEndpoint;
@Value("${sebserver.webservice.api.redirect.unauthorized}")
@ -106,16 +111,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E
@DevGuiProfile
@DevWebServiceProfile
public ClientHttpRequestFactory clientHttpRequestFactory() {
// TODO set connection and read timeout!? configurable!?
return new SimpleClientHttpRequestFactory() {
@Override
protected void prepareConnection(final HttpURLConnection connection, final String httpMethod)
throws IOException {
super.prepareConnection(connection, httpMethod);
connection.setInstanceFollowRedirects(false);
}
};
log.info("Initialize with insecure ClientHttpRequestFactory for development");
return new DevClientHttpRequestFactory();
}
/** A ClientHttpRequestFactory used in production with TSL SSL configuration.
@ -139,15 +136,22 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E
public ClientHttpRequestFactory clientHttpRequestFactoryTLS(final Environment env) throws KeyManagementException,
NoSuchAlgorithmException, KeyStoreException, CertificateException, FileNotFoundException, IOException {
log.info("Initialize with secure ClientHttpRequestFactory for production");
final char[] password = env
.getProperty("sebserver.gui.truststore.pwd")
.getProperty("sebserver.gui.truststore.pwd", "")
.toCharArray();
if (password.length < 3) {
log.error("Missing or incorrect trust-store password: " + String.valueOf(password));
throw new IllegalArgumentException("Missing or incorrect trust-store password");
}
final File trustStoreFile = ResourceUtils.getFile("classpath:truststore.jks");
final SSLContext sslContext = SSLContextBuilder
.create()
.loadTrustMaterial(ResourceUtils.getFile(
"classpath:truststore.jks"),
password)
.loadTrustMaterial(trustStoreFile, password)
.build();
final HttpClient client = HttpClients.custom()
@ -158,4 +162,17 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E
return new HttpComponentsClientHttpRequestFactory(client);
}
// TODO set connection and read timeout!? configurable!?
private static class DevClientHttpRequestFactory extends SimpleClientHttpRequestFactory {
@Override
protected void prepareConnection(
final HttpURLConnection connection,
final String httpMethod) throws IOException {
super.prepareConnection(connection, httpMethod);
connection.setInstanceFollowRedirects(false);
}
}
}

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gbl.model;
import java.io.Serializable;
import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonCreator;
@ -15,7 +17,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
public class EntityKey {
public class EntityKey implements Serializable {
private static final long serialVersionUID = -2368065921846821061L;
@JsonProperty(value = "modelId", required = true)
@NotNull

View file

@ -51,7 +51,7 @@ public class ExamineeAccountDetails {
return this.name;
}
public String getUsername() {
public String getUserName() {
return this.username;
}

View file

@ -36,16 +36,19 @@ public class RAPConfiguration implements ApplicationConfiguration {
@Override
public void configure(final Application application) {
final Map<String, String> properties = new HashMap<>();
properties.put(WebClient.PAGE_TITLE, "SEB Server");
properties.put(WebClient.BODY_HTML, "<big>Loading Application<big>");
// properties.put(WebClient.FAVICON, "icons/favicon.png");
application.addEntryPoint("/gui", RAPSpringEntryPointFactory, properties);
try {
final Map<String, String> properties = new HashMap<>();
properties.put(WebClient.PAGE_TITLE, "SEB Server");
properties.put(WebClient.BODY_HTML, "<big>Loading Application<big>");
// properties.put(WebClient.FAVICON, "icons/favicon.png");
application.addEntryPoint("/gui", RAPSpringEntryPointFactory, properties);
// TODO get file path from properties
application.addStyleSheet(RWT.DEFAULT_THEME_ID, "static/css/sebserver.css");
} catch (final RuntimeException re) {
throw re;
} catch (final Exception e) {
log.error("Error during CSS parsing. Please check the custom CSS files for errors.", e);
}

View file

@ -32,15 +32,7 @@ public class RAPSpringConfig {
@Bean
public ServletContextInitializer initializer() {
return new ServletContextInitializer() {
@Override
public void onStartup(final ServletContext servletContext) throws ServletException {
servletContext.setInitParameter(
"org.eclipse.rap.applicationConfiguration",
RAPConfiguration.class.getName());
}
};
return new RAPServletContextInitializer();
}
@Bean
@ -56,4 +48,13 @@ public class RAPSpringConfig {
return new ServletRegistrationBean<>(new RWTServlet(), this.entrypoint + "/*");
}
private static class RAPServletContextInitializer implements ServletContextInitializer {
@Override
public void onStartup(final ServletContext servletContext) throws ServletException {
servletContext.setInitParameter(
"org.eclipse.rap.applicationConfiguration",
RAPConfiguration.class.getName());
}
}
}

View file

@ -87,8 +87,7 @@ public class InstitutionForm implements TemplateComposer {
final boolean isReadonly = pageContext.isReadonly();
// new PageContext with actual EntityKey
final PageContext formContext = pageContext;
pageContext.withEntityKey(institution.getEntityKey());
final PageContext formContext = pageContext.withEntityKey(institution.getEntityKey());
if (log.isDebugEnabled()) {
log.debug("Institution Form for Institution {}", institution.name);

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gui.content;
import java.util.function.Consumer;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
@ -118,40 +120,22 @@ public class MainPage implements TemplateComposer {
contentObjectslayout.marginHeight = 0;
contentObjectslayout.marginWidth = 0;
contentObjects.setLayout(contentObjectslayout);
contentObjects.setData(PageEventListener.LISTENER_ATTRIBUTE_KEY,
new ActionEventListener() {
@Override
public int priority() {
return 2;
}
@Override
public void notify(final ActionEvent event) {
pageContext.composerService().compose(
event.action.definition.contentPaneComposer,
event.action.pageContext().copyOf(contentObjects));
}
});
contentObjects.setData(
PageEventListener.LISTENER_ATTRIBUTE_KEY,
new ContentActionEventListener(event -> pageContext.composerService().compose(
event.action.definition.contentPaneComposer,
event.action.pageContext().copyOf(contentObjects)), 2));
final Composite actionPane = new Composite(mainSash, SWT.NONE);
actionPane.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
final GridLayout actionPaneGrid = new GridLayout();
actionPane.setLayout(actionPaneGrid);
actionPane.setData(RWT.CUSTOM_VARIANT, "actionPane");
actionPane.setData(PageEventListener.LISTENER_ATTRIBUTE_KEY,
new ActionEventListener() {
@Override
public int priority() {
return 1;
}
@Override
public void notify(final ActionEvent event) {
pageContext.composerService().compose(
event.action.definition.actionPaneComposer,
event.action.pageContext().copyOf(actionPane));
}
});
actionPane.setData(
PageEventListener.LISTENER_ATTRIBUTE_KEY,
new ContentActionEventListener(event -> pageContext.composerService().compose(
event.action.definition.actionPaneComposer,
event.action.pageContext().copyOf(actionPane)), 1));
pageContext.composerService().compose(
ActivitiesPane.class,
@ -160,4 +144,25 @@ public class MainPage implements TemplateComposer {
mainSash.setWeights(DEFAULT_SASH_WEIGHTS);
}
private static final class ContentActionEventListener implements ActionEventListener {
private final int priority;
private final Consumer<ActionEvent> apply;
protected ContentActionEventListener(final Consumer<ActionEvent> apply, final int priority) {
this.apply = apply;
this.priority = priority;
}
@Override
public int priority() {
return this.priority;
}
@Override
public void notify(final ActionEvent event) {
this.apply.accept(event);
}
}
}

View file

@ -112,8 +112,7 @@ public class UserAccountForm implements TemplateComposer {
.getOr(false);
// new PageContext with actual EntityKey
final PageContext formContext = pageContext;
pageContext.withEntityKey(userAccount.getEntityKey());
final PageContext formContext = pageContext.withEntityKey(userAccount.getEntityKey());
if (log.isDebugEnabled()) {
log.debug("UserAccount Form for user {}", userAccount.getName());

View file

@ -120,24 +120,7 @@ public class ActivitiesPane implements TemplateComposer {
navigation.addListener(SWT.Selection, event -> handleSelection(pageContext, event));
navigation.setData(
PageEventListener.LISTENER_ATTRIBUTE_KEY,
new ActionEventListener() {
@Override
public void notify(final ActionEvent event) {
final MainPageState mainPageState = MainPageState.get();
mainPageState.action = event.action;
if (!event.activity) {
final EntityKey entityKey = event.action.getEntityKey();
final String modelId = (entityKey != null) ? entityKey.modelId : null;
final TreeItem item = findItemByActionDefinition(
navigation.getItems(),
event.action.definition,
modelId);
if (item != null) {
navigation.select(item);
}
}
}
});
new ActivitiesActionEventListener(navigation));
// page-selection on (re)load
final MainPageState mainPageState = MainPageState.get();
@ -175,9 +158,12 @@ public class ActivitiesPane implements TemplateComposer {
for (final TreeItem item : items) {
final Action action = getActivitySelection(item);
if (action == null) {
continue;
}
final EntityKey entityKey = action.getEntityKey();
if (action != null
&& (action.definition == actionDefinition || action.definition == actionDefinition.activityAlias) &&
if ((action.definition == actionDefinition || action.definition == actionDefinition.activityAlias) &&
(entityKey == null || (modelId != null && modelId.equals(entityKey.modelId)))) {
return item;
}
@ -212,4 +198,29 @@ public class ActivitiesPane implements TemplateComposer {
item.setData(ATTR_ACTIVITY_SELECTION, action);
}
private final class ActivitiesActionEventListener implements ActionEventListener {
private final Tree navigation;
private ActivitiesActionEventListener(final Tree navigation) {
this.navigation = navigation;
}
@Override
public void notify(final ActionEvent event) {
final MainPageState mainPageState = MainPageState.get();
mainPageState.action = event.action;
if (!event.activity) {
final EntityKey entityKey = event.action.getEntityKey();
final String modelId = (entityKey != null) ? entityKey.modelId : null;
final TreeItem item = findItemByActionDefinition(
this.navigation.getItems(),
event.action.definition,
modelId);
if (item != null) {
this.navigation.select(item);
}
}
}
}
}

View file

@ -76,9 +76,9 @@ public final class SelectionFieldBuilder extends FieldBuilder {
((Control) selection).setLayoutData(gridData);
selection.select(this.value);
if (this.multi) {
builder.form.putField(this.name, lab, (MultiSelection) selection);
builder.form.putField(this.name, lab, selection.<MultiSelection> getTypeInstance());
} else {
builder.form.putField(this.name, lab, (SingleSelection) selection);
builder.form.putField(this.name, lab, selection.<SingleSelection> getTypeInstance());
}
if (this.selectionListener != null) {
((Control) selection).addListener(SWT.Selection, e -> {

View file

@ -15,52 +15,4 @@ public interface ActionEventListener extends PageEventListener<ActionEvent> {
return type == ActionEvent.class;
}
// static ActionEventListener of(final Consumer<ActionEvent> eventConsumer) {
// return new ActionEventListener() {
// @Override
// public void notify(final ActionEvent event) {
// eventConsumer.accept(event);
// }
// };
// }
//
// static ActionEventListener of(
// final Predicate<ActionEvent> predicate,
// final Consumer<ActionEvent> eventConsumer) {
//
// return new ActionEventListener() {
// @Override
// public void notify(final ActionEvent event) {
// if (predicate.test(event)) {
// eventConsumer.accept(event);
// }
// }
// };
// }
// static ActionEventListener of(
// final ActionDefinition actionDefinition,
// final Consumer<ActionEvent> eventConsumer) {
//
// return new ActionEventListener() {
// @Override
// public void notify(final ActionEvent event) {
// if (event.actionDefinition == actionDefinition) {
// eventConsumer.accept(event);
// }
// }
// };
// }
//
// static void injectListener(
// final Widget widget,
// final ActionDefinition actionDefinition,
// final Consumer<ActionEvent> eventConsumer) {
//
// widget.setData(
// PageEventListener.LISTENER_ATTRIBUTE_KEY,
// of(actionDefinition, eventConsumer));
// }
}

View file

@ -28,6 +28,7 @@ public final class MainPageState {
public static MainPageState get() {
try {
final HttpSession httpSession = RWT
.getUISession()
.getHttpSession();
@ -39,6 +40,9 @@ public final class MainPageState {
}
return mainPageState;
} catch (final RuntimeException re) {
throw re;
} catch (final Exception e) {
log.error("Unexpected error while trying to get MainPageState from user-session");
}

View file

@ -138,27 +138,6 @@ public class PageContextImpl implements PageContext {
attrs);
}
// @Override
// public PageContext withSelection(final ActivitySelection selection, final boolean clearAttributes) {
// if (selection == null) {
// return this;
// }
//
// final Map<String, String> attrs = new HashMap<>();
// if (!clearAttributes) {
// attrs.putAll(this.attributes);
// }
// attrs.putAll(selection.getAttributes());
//
// return new PageContextImpl(
// this.restService,
// this.i18nSupport,
// this.composerService,
// this.root,
// this.parent,
// attrs);
// }
@Override
public String getAttribute(final String name) {
return this.attributes.get(name);
@ -268,7 +247,6 @@ public class PageContextImpl implements PageContext {
}
@Override
@SuppressWarnings("serial")
public void applyConfirmDialog(final LocTextKey confirmMessage, final Runnable onOK) {
final Message messageBox = new Message(
this.root.getShell(),
@ -276,30 +254,9 @@ public class PageContextImpl implements PageContext {
this.i18nSupport.getText(confirmMessage),
SWT.OK | SWT.CANCEL);
messageBox.setMarkupEnabled(true);
messageBox.open(new DialogCallback() {
@Override
public void dialogClosed(final int returnCode) {
if (returnCode == SWT.OK) {
try {
onOK.run();
} catch (final Throwable t) {
log.error(
"Unexpected on confirm callback execution. This should not happen, plase secure the given onOK Runnable",
t);
}
}
}
});
messageBox.open(new ConfirmDialogCallback(onOK));
}
// public void applyValidationErrorDialog(final Collection<FieldValidationError> validationErrors) {
// final Message messageBox = new Message(
// this.root.getShell(),
// this.i18nSupport.getText("org.sebserver.dialog.validationErrors.title"),
// this.i18nSupport.getText(confirmMessage),
// SWT.OK);
// }
@Override
public void forwardToPage(
final PageDefinition pageDefinition,
@ -391,6 +348,28 @@ public class PageContextImpl implements PageContext {
+ "]";
}
private final class ConfirmDialogCallback implements DialogCallback {
private static final long serialVersionUID = 1491270214433492441L;
private final Runnable onOK;
private ConfirmDialogCallback(final Runnable onOK) {
this.onOK = onOK;
}
@Override
public void dialogClosed(final int returnCode) {
if (returnCode == SWT.OK) {
try {
this.onOK.run();
} catch (final Throwable t) {
log.error(
"Unexpected on confirm callback execution. This should not happen, plase secure the given onOK Runnable",
t);
}
}
}
}
private static final class ListenerComparator implements Comparator<PageEventListener<?>> {
@Override
public int compare(final PageEventListener<?> o1, final PageEventListener<?> o2) {

View file

@ -91,6 +91,7 @@ public abstract class RestCall<T> {
final RestCallError restCallError =
new RestCallError("Response Entity: " + responseEntity.toString());
restCallError.errors.addAll(RestCall.this.jsonMapper.readValue(
responseEntity.getBody(),
new TypeReference<List<APIMessage>>() {
@ -106,14 +107,25 @@ public abstract class RestCall<T> {
} catch (final Throwable t) {
final RestCallError restCallError = new RestCallError("Unexpected error while rest call", t);
try {
final String responseBody = ((RestClientResponseException) t).getResponseBodyAsString();
restCallError.errors.addAll(RestCall.this.jsonMapper.readValue(
responseBody,
new TypeReference<List<APIMessage>>() {
}));
} catch (final Exception e) {
log.error("Unexpected error-response while webservice API call for: {}", builder, e);
} catch (final ClassCastException cce) {
log.error("Unexpected error-response while webservice API call for: {}", builder, cce);
log.error("Unexpected error-response cause: ", t);
restCallError.errors.add(APIMessage.ErrorMessage.UNEXPECTED.of(cce));
} catch (final RuntimeException re) {
log.error("Unexpected runtime error while webservice API call for: {}", builder, re);
log.error("Unexpected runtime error cause: ", t);
restCallError.errors.add(APIMessage.ErrorMessage.UNEXPECTED.of(re));
} catch (final Exception e) {
log.error("Unexpected error while webservice API call for: {}", builder, e);
log.error("Unexpected error cause: ", t);
restCallError.errors.add(APIMessage.ErrorMessage.UNEXPECTED.of(e));
}

View file

@ -170,14 +170,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
this.restTemplate = new DisposableOAuth2RestTemplate(this.resource);
this.restTemplate.setRequestFactory(clientHttpRequestFactory);
this.restTemplate.setErrorHandler(new OAuth2ErrorHandler(this.resource) {
@Override
public boolean hasError(final ClientHttpResponse response) throws IOException {
final HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
return (statusCode != null && statusCode.series() == HttpStatus.Series.SERVER_ERROR);
}
});
this.restTemplate.setErrorHandler(new ErrorHandler(this.resource));
this.revokeTokenURI = webserviceURIService.getOAuthRevokeTokenURI();
this.currentUserURI = webserviceURIService.getCurrentUserRequestURI();
@ -297,5 +290,17 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
.contains(role.name());
}
private final class ErrorHandler extends OAuth2ErrorHandler {
private ErrorHandler(final OAuth2ProtectedResourceDetails resource) {
super(resource);
}
@Override
public boolean hasError(final ClientHttpResponse response) throws IOException {
final HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
return (statusCode != null && statusCode.series() == HttpStatus.Series.SERVER_ERROR);
}
}
}
}

View file

@ -21,8 +21,9 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
public class TableNavigator {
private final static int PAGE_NAV_SIZE = 3;
private final Composite composite;
private final int pageNavSize = 3;
private final EntityTable<?> entityTable;
TableNavigator(final EntityTable<?> entityTable) {
@ -56,7 +57,7 @@ public class TableNavigator {
createRewardLabel(pageNumber, numNav);
}
for (int i = pageNumber - this.pageNavSize; i < pageNumber + this.pageNavSize; i++) {
for (int i = pageNumber - PAGE_NAV_SIZE; i < pageNumber + PAGE_NAV_SIZE; i++) {
if (i >= 1 && i <= numberOfPages) {
createPageNumberLabel(i, i != pageNumber, numNav);
}

View file

@ -41,7 +41,7 @@ public class ImageUpload extends Composite {
private static final Logger log = LoggerFactory.getLogger(ImageUpload.class);
private final ServerPushService serverPushService;
private transient final ServerPushService serverPushService;
private final Composite imageCanvas;
private final FileUpload fileUpload;
@ -136,8 +136,10 @@ public class ImageUpload extends Composite {
private static final void wait(final ServerPushContext context) {
try {
Thread.sleep(200);
} catch (final Exception e) {
} catch (final InterruptedException e) {
log.info("InterruptedException while wait for image uplaod. Just ignore it");
}
}
private static final void update(final ServerPushContext context) {
@ -145,6 +147,7 @@ public class ImageUpload extends Composite {
if (imageUpload.imageBase64 != null
&& imageUpload.loadNewImage
&& imageUpload.imageLoaded) {
final Base64InputStream input = new Base64InputStream(
new ByteArrayInputStream(
imageUpload.imageBase64.getBytes(StandardCharsets.UTF_8)),

View file

@ -133,4 +133,10 @@ public class MultiSelection extends Composite implements Selection {
deselectAll();
}
@SuppressWarnings("unchecked")
@Override
public MultiSelection getTypeInstance() {
return this;
}
}

View file

@ -24,4 +24,6 @@ public interface Selection {
void setVisible(boolean visible);
<T extends Selection> T getTypeInstance();
}

View file

@ -72,4 +72,10 @@ public class SingleSelection extends Combo implements Selection {
super.setItems(this.valueMapping.toArray(new String[this.valueMapping.size()]));
}
@SuppressWarnings("unchecked")
@Override
public SingleSelection getTypeInstance() {
return this;
}
}

View file

@ -255,6 +255,12 @@ public class PaginationService {
examTableMap.put(
Domain.EXAM.ATTR_STATUS,
ExamRecordDynamicSqlSupport.status.name());
this.sortColumnMapping.put(
ExamRecordDynamicSqlSupport.examRecord.name(),
examTableMap);
this.defaultSortColumn.put(
ExamRecordDynamicSqlSupport.examRecord.name(),
Domain.EXAM.ATTR_ID);
}

View file

@ -16,8 +16,7 @@ public class PermissionDeniedException extends RuntimeException {
private static final long serialVersionUID = 5333137812363042580L;
public final EntityType entityType;
public final GrantEntity entity;
public final PrivilegeType grantType;
public final PrivilegeType privilegeType;
public final String userId;
public PermissionDeniedException(
@ -27,8 +26,7 @@ public class PermissionDeniedException extends RuntimeException {
super("No grant: " + grantType + " on type: " + entityType + " for user: " + userId);
this.entityType = entityType;
this.entity = null;
this.grantType = grantType;
this.privilegeType = grantType;
this.userId = userId;
}
@ -43,8 +41,7 @@ public class PermissionDeniedException extends RuntimeException {
" entity owner: " + entity.getOwnerId() +
" for user: " + userId);
this.entityType = entity.entityType();
this.entity = entity;
this.grantType = grantType;
this.privilegeType = grantType;
this.userId = userId;
}

View file

@ -156,22 +156,14 @@ public class BulkActionService {
this.supporter.get(EntityType.INDICATOR),
this.supporter.get(EntityType.CLIENT_CONNECTION),
this.supporter.get(EntityType.CONFIGURATION_NODE));
case LMS_SETUP:
return Arrays.asList(
this.supporter.get(EntityType.EXAM),
this.supporter.get(EntityType.INDICATOR),
this.supporter.get(EntityType.CLIENT_CONNECTION));
case USER:
return Arrays.asList(
this.supporter.get(EntityType.EXAM),
this.supporter.get(EntityType.INDICATOR),
this.supporter.get(EntityType.CLIENT_CONNECTION),
this.supporter.get(EntityType.CONFIGURATION_NODE));
case LMS_SETUP:
case EXAM:
return Arrays.asList(
this.supporter.get(EntityType.EXAM),
this.supporter.get(EntityType.INDICATOR),
this.supporter.get(EntityType.CLIENT_CONNECTION));
case CONFIGURATION:
return Arrays.asList(
this.supporter.get(EntityType.EXAM),