/*
* Copyright (c) 2010-2015 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.evolveum.midpoint.web.page.admin.configuration;
import com.evolveum.midpoint.gui.api.model.NonEmptyModel;
import com.evolveum.midpoint.gui.api.model.NonEmptyWrapperModel;
import com.evolveum.midpoint.gui.api.page.PageBase;
import com.evolveum.midpoint.gui.api.util.WebComponentUtil;
import com.evolveum.midpoint.model.common.expression.ExpressionUtil;
import com.evolveum.midpoint.model.common.expression.ExpressionVariables;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.marshaller.QueryConvertor;
import com.evolveum.midpoint.prism.query.ObjectFilter;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.QueryJaxbConvertor;
import com.evolveum.midpoint.schema.GetOperationOptions;
import com.evolveum.midpoint.schema.RepositoryQueryDiagRequest;
import com.evolveum.midpoint.schema.RepositoryQueryDiagResponse;
import com.evolveum.midpoint.schema.SchemaConstantsGenerated;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.security.api.AuthorizationConstants;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.exception.CommonException;
import com.evolveum.midpoint.util.exception.ExpressionEvaluationException;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.logging.LoggingUtils;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.web.application.AuthorizationAction;
import com.evolveum.midpoint.web.application.PageDescriptor;
import com.evolveum.midpoint.web.component.AceEditor;
import com.evolveum.midpoint.web.component.AjaxSubmitButton;
import com.evolveum.midpoint.web.component.input.QNameChoiceRenderer;
import com.evolveum.midpoint.web.component.search.Search;
import com.evolveum.midpoint.web.component.search.SearchFactory;
import com.evolveum.midpoint.web.component.util.VisibleEnableBehaviour;
import com.evolveum.midpoint.web.page.admin.configuration.dto.RepoQueryDto;
import com.evolveum.midpoint.web.security.MidPointAuthWebSession;
import com.evolveum.midpoint.web.session.PageStorage;
import com.evolveum.midpoint.web.session.SessionStorage;
import com.evolveum.midpoint.web.util.StringResourceChoiceRenderer;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import com.evolveum.prism.xml.ns._public.query_3.QueryType;
import com.evolveum.prism.xml.ns._public.query_3.SearchFilterType;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.OnChangeAjaxBehavior;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.model.util.ListModel;
import javax.xml.namespace.QName;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
/**
* @author lazyman
* @author mederly
*/
@PageDescriptor(url = "/admin/config/repositoryQuery", action = {
@AuthorizationAction(actionUri = PageAdminConfiguration.AUTH_CONFIGURATION_ALL,
label = PageAdminConfiguration.AUTH_CONFIGURATION_ALL_LABEL, description = PageAdminConfiguration.AUTH_CONFIGURATION_ALL_DESCRIPTION),
@AuthorizationAction(actionUri = AuthorizationConstants.AUTZ_UI_CONFIGURATION_REPOSITORY_QUERY_URL,
label = "PageRepositoryQuery.auth.query.label", description = "PageRepositoryQuery.auth.query.description")
})
public class PageRepositoryQuery extends PageAdminConfiguration {
private static final Trace LOGGER = TraceManager.getTrace(PageRepositoryQuery.class);
private static final String DOT_CLASS = PageRepositoryQuery.class.getName() + ".";
private static final String OPERATION_CHECK_QUERY = DOT_CLASS + "checkQuery";
private static final String OPERATION_TRANSLATE_QUERY = DOT_CLASS + "translateQuery";
private static final String OPERATION_EXECUTE_QUERY = DOT_CLASS + "executeQuery";
private static final String ID_MAIN_FORM = "mainForm";
private static final String ID_MIDPOINT_QUERY_BUTTON_BAR = "midPointQueryButtonBar";
private static final String ID_EXECUTE_MIDPOINT = "executeMidPoint";
private static final String ID_COMPILE_MIDPOINT = "compileMidPoint";
private static final String ID_USE_IN_OBJECT_LIST = "useInObjectList";
private static final String ID_EXECUTE_HIBERNATE = "executeHibernate";
private static final String ID_EDITOR_MIDPOINT = "editorMidPoint";
private static final String ID_EDITOR_HIBERNATE = "editorHibernate";
private static final String ID_HIBERNATE_PARAMETERS = "hibernateParameters";
private static final String ID_RESULT_LABEL = "resultLabel";
private static final String ID_RESULT_TEXT = "resultText";
private static final String ID_QUERY_SAMPLE = "querySample";
private static final String ID_OBJECT_TYPE = "objectType";
private static final String ID_HIBERNATE_PARAMETERS_NOTE = "hibernateParametersNote";
private static final String ID_INCOMPLETE_RESULTS_NOTE = "incompleteResultsNote";
private static final String SAMPLES_DIR = "query-samples";
private static final List<String> SAMPLES = Arrays.asList(
"UserType_AllUsers",
"UserType_UsersStartingWithA",
"UserType_UsersContainingJack",
"UserType_UsersNamedJack",
"UserType_First10UsersStartingWithA",
"UserType_UsersWithAGivenMailDomain",
"UserType_SpecifiedCostCenters",
"UserType_UsersThatHaveAssignedRole",
"UserType_UsersThatHaveARole",
"OrgType_AllRootOrgs",
"OrgType_OrgOfType1",
"ObjectType_AllObjectsInASubtree",
"ObjectType_AllObjectsInAnOrg",
"ShadowType_ShadowsOnGivenResource",
"UserType_UsersWithShadowOnGivenResource"
);
private static final Set<QName> USE_IN_OBJECT_LIST_AVAILABLE_FOR = new HashSet<>(Arrays.asList(
UserType.COMPLEX_TYPE,
RoleType.COMPLEX_TYPE,
ServiceType.COMPLEX_TYPE,
ResourceType.COMPLEX_TYPE
));
private final NonEmptyModel<RepoQueryDto> model = new NonEmptyWrapperModel<>(new Model<>(new RepoQueryDto()));
private final boolean isAdmin;
enum Action {TRANSLATE_ONLY, EXECUTE_MIDPOINT, EXECUTE_HIBERNATE }
public PageRepositoryQuery() {
this(null, null);
}
public PageRepositoryQuery(QName objectType, String queryText) {
model.getObject().setObjectType(objectType);
model.getObject().setMidPointQuery(queryText);
boolean admin;
try {
admin = getSecurityEnforcer().isAuthorized(AuthorizationConstants.AUTZ_ALL_URL, null, null, null, null, null);
} catch (SchemaException | RuntimeException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't determine admin authorization -- continuing as non-admin", e);
admin = false;
}
isAdmin = admin;
initLayout();
}
private void initLayout() {
Form mainForm = new Form(ID_MAIN_FORM);
add(mainForm);
List<QName> objectTypeList = WebComponentUtil.createObjectTypeList();
Collections.sort(objectTypeList, new Comparator<QName>() {
@Override
public int compare(QName o1, QName o2) {
return String.CASE_INSENSITIVE_ORDER.compare(o1.getLocalPart(), o2.getLocalPart());
}
});
DropDownChoice<QName> objectTypeChoice = new DropDownChoice<>(ID_OBJECT_TYPE,
new PropertyModel<QName>(model, RepoQueryDto.F_OBJECT_TYPE),
new ListModel<>(objectTypeList),
new QNameChoiceRenderer());
objectTypeChoice.setOutputMarkupId(true);
objectTypeChoice.setNullValid(true);
objectTypeChoice.add(new OnChangeAjaxBehavior() {
@Override
protected void onUpdate(AjaxRequestTarget target) {
target.add(get(ID_MAIN_FORM).get(ID_MIDPOINT_QUERY_BUTTON_BAR));
}
});
mainForm.add(objectTypeChoice);
AceEditor editorMidPoint = new AceEditor(ID_EDITOR_MIDPOINT, new PropertyModel<String>(model, RepoQueryDto.F_MIDPOINT_QUERY));
editorMidPoint.setHeight(400);
editorMidPoint.setResizeToMaxHeight(false);
mainForm.add(editorMidPoint);
AceEditor editorHibernate = new AceEditor(ID_EDITOR_HIBERNATE, new PropertyModel<String>(model, RepoQueryDto.F_HIBERNATE_QUERY));
editorHibernate.setHeight(300);
editorHibernate.setResizeToMaxHeight(false);
editorHibernate.setReadonly(!isAdmin);
editorHibernate.setMode(null);
mainForm.add(editorHibernate);
AceEditor hibernateParameters = new AceEditor(ID_HIBERNATE_PARAMETERS, new PropertyModel<String>(model, RepoQueryDto.F_HIBERNATE_PARAMETERS));
hibernateParameters.setReadonly(true);
hibernateParameters.setHeight(100);
hibernateParameters.setResizeToMaxHeight(false);
hibernateParameters.setMode(null);
mainForm.add(hibernateParameters);
WebMarkupContainer hibernateParametersNote = new WebMarkupContainer(ID_HIBERNATE_PARAMETERS_NOTE);
hibernateParametersNote.setVisible(isAdmin);
mainForm.add(hibernateParametersNote);
WebMarkupContainer midPointQueryButtonBar = new WebMarkupContainer(ID_MIDPOINT_QUERY_BUTTON_BAR);
midPointQueryButtonBar.setOutputMarkupId(true);
mainForm.add(midPointQueryButtonBar);
AjaxSubmitButton executeMidPoint = new AjaxSubmitButton(ID_EXECUTE_MIDPOINT, createStringResource("PageRepositoryQuery.button.translateAndExecute")) {
@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(getFeedbackPanel());
}
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
queryPerformed(Action.EXECUTE_MIDPOINT, target);
}
};
midPointQueryButtonBar.add(executeMidPoint);
AjaxSubmitButton compileMidPoint = new AjaxSubmitButton(ID_COMPILE_MIDPOINT, createStringResource("PageRepositoryQuery.button.translate")) {
@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(getFeedbackPanel());
}
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
queryPerformed(Action.TRANSLATE_ONLY, target);
}
};
midPointQueryButtonBar.add(compileMidPoint);
AjaxSubmitButton useInObjectList = new AjaxSubmitButton(ID_USE_IN_OBJECT_LIST, createStringResource("PageRepositoryQuery.button.useInObjectList")) {
@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(getFeedbackPanel());
}
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
useInObjectListPerformed(target);
}
};
useInObjectList.add(new VisibleEnableBehaviour() {
@Override
public boolean isVisible() {
return USE_IN_OBJECT_LIST_AVAILABLE_FOR.contains(model.getObject().getObjectType());
}
});
midPointQueryButtonBar.add(useInObjectList);
final DropDownChoice<String> sampleChoice = new DropDownChoice<>(ID_QUERY_SAMPLE,
Model.of(""),
new AbstractReadOnlyModel<List<String>>() {
@Override
public List<String> getObject() {
return SAMPLES;
}
},
new StringResourceChoiceRenderer("PageRepositoryQuery.sample"));
sampleChoice.setNullValid(true);
sampleChoice.add(new OnChangeAjaxBehavior() {
@Override
protected void onUpdate(AjaxRequestTarget target) {
String sampleName = sampleChoice.getModelObject();
if (StringUtils.isEmpty(sampleName)) {
return;
}
String resourceName = SAMPLES_DIR + "/" + sampleName + ".xml.data";
InputStream is = PageRepositoryQuery.class.getResourceAsStream(resourceName);
if (is != null) {
try {
String localTypeName = StringUtils.substringBefore(sampleName, "_");
model.getObject().setObjectType(new QName(SchemaConstants.NS_C, localTypeName));
model.getObject().setMidPointQuery(IOUtils.toString(is, "UTF-8"));
model.getObject().setHibernateQuery("");
model.getObject().setHibernateParameters("");
model.getObject().setQueryResultObject(null);
model.getObject().resetQueryResultText();
target.add(PageRepositoryQuery.this);
} catch (IOException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't read sample from resource {}", e, resourceName);
}
} else {
LOGGER.warn("Resource {} containing sample couldn't be found", resourceName);
}
}
});
mainForm.add(sampleChoice);
AjaxSubmitButton executeHibernate = new AjaxSubmitButton(ID_EXECUTE_HIBERNATE, createStringResource("PageRepositoryQuery.button.execute")) {
@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
target.add(getFeedbackPanel());
}
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
queryPerformed(Action.EXECUTE_HIBERNATE, target);
}
};
executeHibernate.setVisible(isAdmin);
mainForm.add(executeHibernate);
Label resultLabel = new Label(ID_RESULT_LABEL, new AbstractReadOnlyModel<String>() {
@Override
public String getObject() {
if (model.getObject().getQueryResultText() == null) {
return "";
}
Object queryResult = model.getObject().getQueryResultObject();
if (queryResult instanceof List) {
return getString("PageRepositoryQuery.resultObjects", ((List) queryResult).size());
} else if (queryResult instanceof Throwable) {
return getString("PageRepositoryQuery.resultException", queryResult.getClass().getName());
} else {
// including null
return getString("PageRepositoryQuery.result");
}
}
});
mainForm.add(resultLabel);
WebMarkupContainer incompleteResultsNote = new WebMarkupContainer(ID_INCOMPLETE_RESULTS_NOTE);
incompleteResultsNote.add(new VisibleEnableBehaviour() {
@Override
public boolean isVisible() {
return !isAdmin && model.getObject().getQueryResultText() != null;
}
});
mainForm.add(incompleteResultsNote);
AceEditor resultText = new AceEditor(ID_RESULT_TEXT, new PropertyModel<String>(model, RepoQueryDto.F_QUERY_RESULT_TEXT));
resultText.setReadonly(true);
resultText.setHeight(300);
resultText.setResizeToMaxHeight(false);
resultText.setMode(null);
resultText.add(new VisibleEnableBehaviour() {
@Override
public boolean isVisible() {
return model.getObject().getQueryResultText() != null;
}
});
mainForm.add(resultText);
}
private void useInObjectListPerformed(AjaxRequestTarget target) {
final RepoQueryDto dto = model.getObject();
String queryText = dto.getMidPointQuery();
if (StringUtils.isBlank(queryText)) {
queryText = "<query/>";
}
RepositoryQueryDiagRequest request = new RepositoryQueryDiagRequest();
Task task = createSimpleTask(OPERATION_CHECK_QUERY);
OperationResult result = task.getResult();
try {
updateRequestWithMidpointQuery(request, dto.getObjectType(), queryText, task, result); // just to parse the query
ObjectFilter parsedFilter = request.getQuery().getFilter();
String filterAsString;
if (parsedFilter != null) {
SearchFilterType filterType = QueryConvertor.createSearchFilterType(parsedFilter, getPrismContext());
filterAsString = getPrismContext().xmlSerializer().serializeRealValue(filterType, SchemaConstantsGenerated.Q_FILTER);
// TODO remove extra xmlns from serialized value
} else {
filterAsString = "";
}
Class<? extends PageBase> listPageClass = WebComponentUtil.getObjectListPage(request.getType());
String storageKey = listPageClass != null ? WebComponentUtil.getStorageKeyForPage(listPageClass) : null;
if (storageKey == null) {
// shouldn't occur because of button visibility
error("No page to redirect for " + dto.getObjectType());
target.add(getFeedbackPanel());
return;
}
Search search = SearchFactory.createSearch(request.getType(), getPrismContext(), getModelInteractionService());
search.setAdvancedQuery(filterAsString);
search.setShowAdvanced(true);
if (!search.isAdvancedQueryValid(getPrismContext())) {
// shouldn't occur because the query was already parsed
error("Query is not valid: " + search.getAdvancedError());
target.add(getFeedbackPanel());
return;
}
SessionStorage sessionStorage = ((MidPointAuthWebSession) getSession()).getSessionStorage();
PageStorage storage = sessionStorage.getPageStorageMap().get(storageKey);
if (storage == null) {
storage = sessionStorage.initPageStorage(storageKey);
}
storage.setSearch(search);
setResponsePage(listPageClass);
} catch (CommonException | RuntimeException e) {
result.recordFatalError("Couldn't parse query: " + e.getMessage(), e);
showResult(result);
target.add(this);
}
}
private void queryPerformed(Action action, AjaxRequestTarget target) {
String opName = action == Action.TRANSLATE_ONLY ? OPERATION_TRANSLATE_QUERY : OPERATION_EXECUTE_QUERY;
Task task = createSimpleTask(opName);
OperationResult result = new OperationResult(opName);
RepoQueryDto dto = model.getObject();
try {
boolean queryPresent;
RepositoryQueryDiagRequest request = new RepositoryQueryDiagRequest();
switch (action) {
case EXECUTE_HIBERNATE:
String hqlText = dto.getHibernateQuery();
queryPresent = StringUtils.isNotBlank(hqlText);
request.setImplementationLevelQuery(hqlText);
break;
case TRANSLATE_ONLY:
request.setTranslateOnly(true);
case EXECUTE_MIDPOINT:
queryPresent = StringUtils.isNotBlank(dto.getMidPointQuery());
if (queryPresent) {
updateRequestWithMidpointQuery(request, dto.getObjectType(), dto.getMidPointQuery(), task, result);
}
break;
default:
throw new IllegalArgumentException("Invalid action: " + action);
}
if (!queryPresent) {
warnNoQuery(target);
return;
}
RepositoryQueryDiagResponse response;
List<?> queryResult;
if (isAdmin) {
response = getModelDiagnosticService().executeRepositoryQuery(request, task, result);
queryResult = response.getQueryResult();
} else {
request.setTranslateOnly(true);
request.setImplementationLevelQuery(null); // just to be sure
response = getModelDiagnosticService().executeRepositoryQuery(request, task, result);
if (action != Action.TRANSLATE_ONLY) {
// not an admin, so have to fetch objects via model
queryResult = getModelService().searchObjects(request.getType(), request.getQuery(),
GetOperationOptions.createRawCollection(), task, result);
} else {
queryResult = null;
}
}
if (action != Action.EXECUTE_HIBERNATE) {
dto.setHibernateQuery(String.valueOf(response.getImplementationLevelQuery()));
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, RepositoryQueryDiagResponse.ParameterValue> entry : response.getImplementationLevelQueryParameters().entrySet()) {
sb.append(entry.getKey()).append(" = ").append(entry.getValue().displayValue).append("\n");
}
dto.setHibernateParameters(sb.toString());
} else {
dto.setHibernateParameters("");
}
if (action != Action.TRANSLATE_ONLY) {
dto.setQueryResultText(formatQueryResult(queryResult));
dto.setQueryResultObject(queryResult);
} else {
dto.resetQueryResultText();
dto.setQueryResultObject(null);
}
} catch (CommonException | RuntimeException e) {
result.recordFatalError("Couldn't execute query", e);
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't execute query", e);
dto.setQueryResultText(e.getMessage());
dto.setQueryResultObject(e);
} finally {
result.computeStatus();
}
showResult(result);
target.add(this);
}
private void warnNoQuery(AjaxRequestTarget target) {
warn(getString("PageRepositoryQuery.message.emptyString"));
target.add(getFeedbackPanel());
}
private void updateRequestWithMidpointQuery(RepositoryQueryDiagRequest request, QName objectType, String queryText,
Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException {
PrismContext prismContext = getPrismContext();
if (objectType == null) {
objectType = ObjectType.COMPLEX_TYPE;
}
@SuppressWarnings("unchecked")
Class<? extends ObjectType> clazz = (Class<? extends ObjectType>) prismContext.getSchemaRegistry().getCompileTimeClassForObjectType(objectType);
if (clazz == null) {
throw new SchemaException("Couldn't find compile-time class for object type of " + objectType);
}
QueryType queryType = prismContext.parserFor(queryText).xml().parseRealValue(QueryType.class);
request.setType(clazz);
ObjectQuery objectQuery = QueryJaxbConvertor.createObjectQuery(clazz, queryType, prismContext);
ObjectQuery queryWithExprEvaluated = ExpressionUtil.evaluateQueryExpressions(objectQuery, new ExpressionVariables(),
getExpressionFactory(), getPrismContext(), "evaluate query expressions", task, result);
request.setQuery(queryWithExprEvaluated);
}
private String formatQueryResult(List<?> objects) {
StringBuilder sb = new StringBuilder();
if (objects != null) {
for (Object item : objects) {
if (item instanceof Object[]) {
boolean first = true;
for (Object item1 : (Object[]) item) {
if (first) {
first = false;
} else {
sb.append(",");
}
sb.append(item1);
}
} else {
sb.append(item);
}
sb.append("\n");
}
}
return sb.toString();
}
}