blob: 63cccfc0a1a2f8cecb01247cc85c28ad60a5399a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.kylin.rest.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.kylin.common.persistence.RootPersistentEntity;
import org.apache.kylin.cube.CubeInstance;
import org.apache.kylin.cube.model.CubeDesc;
import org.apache.kylin.job.JoinedFormatter;
import org.apache.kylin.metadata.ModifiedOrder;
import org.apache.kylin.metadata.draft.Draft;
import org.apache.kylin.metadata.model.DataModelDesc;
import org.apache.kylin.metadata.model.ISourceAware;
import org.apache.kylin.metadata.model.JoinsTree;
import org.apache.kylin.metadata.model.ModelDimensionDesc;
import org.apache.kylin.metadata.model.TableDesc;
import org.apache.kylin.metadata.model.TblColRef;
import org.apache.kylin.metadata.project.ProjectInstance;
import org.apache.kylin.metadata.util.ModelUtil;
import org.apache.kylin.rest.exception.BadRequestException;
import org.apache.kylin.rest.exception.ForbiddenException;
import org.apache.kylin.rest.msg.Message;
import org.apache.kylin.rest.msg.MsgPicker;
import org.apache.kylin.rest.util.AclEvaluate;
import org.apache.kylin.rest.util.ValidateUtil;
import org.apache.kylin.shaded.com.google.common.collect.Maps;
import org.apache.kylin.shaded.com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/**
* @author jiazhong
*/
@Component("modelMgmtService")
public class ModelService extends BasicService {
private static final Logger logger = LoggerFactory.getLogger(ModelService.class);
@Autowired
@Qualifier("cubeMgmtService")
private CubeService cubeService;
@Autowired
private AclEvaluate aclEvaluate;
public boolean isModelNameValidate(final String modelName) {
if (StringUtils.isEmpty(modelName) || !ValidateUtil.isAlphanumericUnderscore(modelName)) {
return false;
}
for (DataModelDesc model : getDataModelManager().getModels()) {
if (modelName.equalsIgnoreCase(model.getName())) {
return false;
}
}
return true;
}
public List<DataModelDesc> listAllModels(final String modelName, final String projectName, boolean exactMatch)
throws IOException {
List<DataModelDesc> models;
if (null == projectName) {
aclEvaluate.checkIsGlobalAdmin();
models = getDataModelManager().getModels();
} else {
aclEvaluate.checkProjectReadPermission(projectName);
models = getDataModelManager().getModels(projectName);
}
List<DataModelDesc> filterModels = new ArrayList<DataModelDesc>();
for (DataModelDesc modelDesc : models) {
boolean isModelMatch = (null == modelName) || modelName.length() == 0
|| (exactMatch
&& modelDesc.getName().toLowerCase(Locale.ROOT).equals(modelName.toLowerCase(Locale.ROOT)))
|| (!exactMatch && modelDesc.getName().toLowerCase(Locale.ROOT)
.contains(modelName.toLowerCase(Locale.ROOT)));
if (isModelMatch) {
filterModels.add(modelDesc);
}
}
Collections.sort(filterModels, new ModifiedOrder());
return filterModels;
}
public List<DataModelDesc> getModels(final String modelName, final String projectName, final Integer limit,
final Integer offset) throws IOException {
List<DataModelDesc> modelDescs = listAllModels(modelName, projectName, true);
if (limit == null || offset == null) {
return modelDescs;
}
if ((modelDescs.size() - offset) < limit) {
return modelDescs.subList(offset, modelDescs.size());
}
return modelDescs.subList(offset, offset + limit);
}
public DataModelDesc createModelDesc(String projectName, DataModelDesc desc) throws IOException {
aclEvaluate.checkProjectWritePermission(projectName);
Message msg = MsgPicker.getMsg();
if (getDataModelManager().getDataModelDesc(desc.getName()) != null) {
throw new BadRequestException(String.format(Locale.ROOT, msg.getDUPLICATE_MODEL_NAME(), desc.getName()));
}
validateModel(projectName, desc);
DataModelDesc createdDesc = null;
String owner = SecurityContextHolder.getContext().getAuthentication().getName();
createdDesc = getDataModelManager().createDataModelDesc(desc, projectName, owner);
return createdDesc;
}
public DataModelDesc updateModelAndDesc(String project, DataModelDesc desc) throws IOException {
aclEvaluate.checkProjectWritePermission(project);
validateModel(project, desc);
checkModelCompatibility(project, desc);
getDataModelManager().updateDataModelDesc(desc);
return desc;
}
public void checkModelCompatibility(String project, DataModelDesc dataModalDesc) {
ProjectInstance prjInstance = getProjectManager().getProject(project);
if (prjInstance == null) {
throw new BadRequestException("Project " + project + " does not exist");
}
if (!prjInstance.getConfig().isModelSchemaUpdaterCheckerEnabled()) {
logger.info("Skip the check for model schema update");
return;
}
ModelSchemaUpdateChecker checker = new ModelSchemaUpdateChecker(getTableManager(), getCubeManager(),
getDataModelManager());
ModelSchemaUpdateChecker.CheckResult result = checker.allowEdit(dataModalDesc, project);
result.raiseExceptionWhenInvalid();
}
public void checkModelCompatibility(DataModelDesc dataModalDesc, List<TableDesc> tableDescList) {
ModelSchemaUpdateChecker checker = new ModelSchemaUpdateChecker(getTableManager(), getCubeManager(),
getDataModelManager());
Map<String, TableDesc> tableDescMap = Maps.newHashMapWithExpectedSize(tableDescList.size());
for (TableDesc tableDesc : tableDescList) {
tableDescMap.put(tableDesc.getIdentity(), tableDesc);
}
dataModalDesc.init(getConfig(), tableDescMap);
ModelSchemaUpdateChecker.CheckResult result = checker.allowEdit(dataModalDesc, null, false);
result.raiseExceptionWhenInvalid();
}
public void validateModel(String project, DataModelDesc desc) throws IllegalArgumentException {
String factTableName = desc.getRootFactTableName();
TableDesc tableDesc = getTableManager().getTableDesc(factTableName, project);
if (!StringUtils.isEmpty(desc.getFilterCondition())) {
try {
JoinedFormatter formatter = new JoinedFormatter(true);
ModelUtil.verifyFilterCondition(factTableName, formatter.formatSentence(desc.getFilterCondition()),
tableDesc);
} catch (Exception e) {
throw new BadRequestException(e.toString());
}
}
if ((tableDesc.getSourceType() == ISourceAware.ID_STREAMING || tableDesc.isStreamingTable())
&& (desc.getPartitionDesc() == null || desc.getPartitionDesc().getPartitionDateColumn() == null)) {
throw new IllegalArgumentException("Must define a partition column.");
}
}
public void dropModel(DataModelDesc desc) throws IOException {
aclEvaluate.checkProjectWritePermission(desc.getProjectInstance().getName());
Message msg = MsgPicker.getMsg();
//check cube desc exist
List<CubeDesc> cubeDescs = getCubeDescManager().listAllDesc();
for (CubeDesc cubeDesc : cubeDescs) {
if (cubeDesc.getModelName().equals(desc.getName())) {
throw new BadRequestException(
String.format(Locale.ROOT, msg.getDROP_REFERENCED_MODEL(), cubeDesc.getName()));
}
}
getDataModelManager().dropModel(desc);
}
public boolean isTableInAnyModel(TableDesc table) {
return getDataModelManager().isTableInAnyModel(table);
}
public boolean isTableInModel(TableDesc table, String project) throws IOException {
return getDataModelManager().getModelsUsingTable(table, project).size() > 0;
}
public List<String> getModelsUsingTable(TableDesc table, String project) throws IOException {
return getDataModelManager().getModelsUsingTable(table, project);
}
public Map<TblColRef, Set<CubeInstance>> getUsedDimCols(String modelName, String project) {
Map<TblColRef, Set<CubeInstance>> ret = Maps.newHashMap();
List<CubeInstance> cubeInstances = cubeService.listAllCubes(null, project, modelName, true);
for (CubeInstance cubeInstance : cubeInstances) {
CubeDesc cubeDesc = cubeInstance.getDescriptor();
for (TblColRef tblColRef : cubeDesc.listDimensionColumnsIncludingDerived()) {
if (ret.containsKey(tblColRef)) {
ret.get(tblColRef).add(cubeInstance);
} else {
Set<CubeInstance> set = Sets.newHashSet(cubeInstance);
ret.put(tblColRef, set);
}
}
}
return ret;
}
public Map<TblColRef, Set<CubeInstance>> getUsedNonDimCols(String modelName, String project) {
Map<TblColRef, Set<CubeInstance>> ret = Maps.newHashMap();
List<CubeInstance> cubeInstances = cubeService.listAllCubes(null, project, modelName, true);
for (CubeInstance cubeInstance : cubeInstances) {
CubeDesc cubeDesc = cubeInstance.getDescriptor();
Set<TblColRef> tblColRefs = Sets.newHashSet(cubeDesc.listAllColumns());//make a copy
tblColRefs.removeAll(cubeDesc.listDimensionColumnsIncludingDerived());
for (TblColRef tblColRef : tblColRefs) {
if (ret.containsKey(tblColRef)) {
ret.get(tblColRef).add(cubeInstance);
} else {
Set<CubeInstance> set = Sets.newHashSet(cubeInstance);
ret.put(tblColRef, set);
}
}
}
return ret;
}
private List<String> getModelCols(DataModelDesc model) {
List<String> dimCols = new ArrayList<String>();
List<ModelDimensionDesc> dimensions = model.getDimensions();
for (ModelDimensionDesc dim : dimensions) {
String table = dim.getTable();
for (String c : dim.getColumns()) {
dimCols.add(table + "." + c);
}
}
return dimCols;
}
private List<String> getModelMeasures(DataModelDesc model) {
List<String> measures = new ArrayList<String>();
for (String s : model.getMetrics()) {
measures.add(s);
}
return measures;
}
private Map<String, List<String>> getInfluencedCubesByDims(List<String> dims, List<CubeInstance> cubes) {
Map<String, List<String>> influencedCubes = new HashMap<>();
for (CubeInstance cubeInstance : cubes) {
CubeDesc cubeDesc = cubeInstance.getDescriptor();
for (TblColRef tblColRef : cubeDesc.listDimensionColumnsIncludingDerived()) {
if (dims.contains(tblColRef.getIdentity()))
continue;
if (influencedCubes.get(tblColRef.getIdentity()) == null) {
List<String> candidates = new ArrayList<>();
candidates.add(cubeInstance.getName());
influencedCubes.put(tblColRef.getIdentity(), candidates);
} else
influencedCubes.get(tblColRef.getIdentity()).add(cubeInstance.getName());
}
}
return influencedCubes;
}
private Map<String, List<String>> getInfluencedCubesByMeasures(List<String> allCols, List<CubeInstance> cubes) {
Map<String, List<String>> influencedCubes = new HashMap<>();
for (CubeInstance cubeInstance : cubes) {
CubeDesc cubeDesc = cubeInstance.getDescriptor();
Set<TblColRef> tblColRefs = Sets.newHashSet(cubeDesc.listAllColumns());
tblColRefs.removeAll(cubeDesc.listDimensionColumnsIncludingDerived());
for (TblColRef tblColRef : tblColRefs) {
if (allCols.contains(tblColRef.getIdentity()))
continue;
if (influencedCubes.get(tblColRef.getIdentity()) == null) {
List<String> candidates = new ArrayList<>();
candidates.add(cubeInstance.getName());
influencedCubes.put(tblColRef.getIdentity(), candidates);
} else
influencedCubes.get(tblColRef.getIdentity()).add(cubeInstance.getName());
}
}
return influencedCubes;
}
private String checkIfBreakExistingCubes(DataModelDesc dataModelDesc, String project) throws IOException {
String modelName = dataModelDesc.getName();
List<CubeInstance> cubes = cubeService.listAllCubes(null, project, modelName, true);
List<DataModelDesc> historyModels = listAllModels(modelName, project, true);
StringBuilder checkRet = new StringBuilder();
if (cubes != null && cubes.size() != 0 && !historyModels.isEmpty()) {
dataModelDesc.init(getConfig(), getTableManager().getAllTablesMap(project));
List<String> curModelDims = getModelCols(dataModelDesc);
List<String> curModelMeasures = getModelMeasures(dataModelDesc);
List<String> curModelDimsAndMeasures = new ArrayList<>();
curModelDimsAndMeasures.addAll(curModelDims);
curModelDimsAndMeasures.addAll(curModelMeasures);
Map<String, List<String>> influencedCubesByDims = getInfluencedCubesByDims(curModelDims, cubes);
Map<String, List<String>> influencedCubesByMeasures = getInfluencedCubesByMeasures(curModelDimsAndMeasures,
cubes);
for (Map.Entry<String, List<String>> e : influencedCubesByDims.entrySet()) {
checkRet.append("Dimension: ");
checkRet.append(e.getKey());
checkRet.append(" can't be removed, It is referred in Cubes: ");
checkRet.append(e.getValue().toString());
checkRet.append("\r\n");
}
for (Map.Entry<String, List<String>> e : influencedCubesByMeasures.entrySet()) {
checkRet.append("Measure: ");
checkRet.append(e.getKey());
checkRet.append(" can't be removed, It is referred in Cubes: ");
checkRet.append(e.getValue().toString());
checkRet.append("\r\n");
}
DataModelDesc originDataModelDesc = historyModels.get(0);
if (!dataModelDesc.getRootFactTable().equals(originDataModelDesc.getRootFactTable()))
checkRet.append("Root fact table can't be modified. \r\n");
JoinsTree joinsTree = dataModelDesc.getJoinsTree(), originJoinsTree = originDataModelDesc.getJoinsTree();
if (joinsTree.matchNum(originJoinsTree) != originDataModelDesc.getJoinTables().length + 1)
checkRet.append("The join shouldn't be modified in this model.");
}
return checkRet.toString();
}
public void primaryCheck(DataModelDesc modelDesc) {
Message msg = MsgPicker.getMsg();
if (modelDesc == null) {
throw new BadRequestException(msg.getINVALID_MODEL_DEFINITION());
}
String modelName = modelDesc.getName();
if (StringUtils.isEmpty(modelName)) {
logger.info("Model name should not be empty.");
throw new BadRequestException(msg.getEMPTY_MODEL_NAME());
}
if (!ValidateUtil.isAlphanumericUnderscore(modelName)) {
logger.info("Invalid model name {}, only letters, numbers and underscore supported.", modelDesc.getName());
throw new BadRequestException(String.format(Locale.ROOT, msg.getINVALID_MODEL_NAME(), modelName));
}
}
public DataModelDesc updateModelToResourceStore(DataModelDesc modelDesc, String projectName) throws IOException {
aclEvaluate.checkProjectWritePermission(projectName);
Message msg = MsgPicker.getMsg();
modelDesc.setDraft(false);
if (modelDesc.getUuid() == null)
modelDesc.updateRandomUuid();
try {
if (modelDesc.getLastModified() == 0) {
// new
modelDesc = createModelDesc(projectName, modelDesc);
} else {
// update
String error = checkIfBreakExistingCubes(modelDesc, projectName);
if (!error.isEmpty()) {
throw new BadRequestException(error);
}
modelDesc = updateModelAndDesc(projectName, modelDesc);
}
} catch (AccessDeniedException accessDeniedException) {
throw new ForbiddenException(msg.getUPDATE_MODEL_NO_RIGHT());
}
if (!modelDesc.getError().isEmpty()) {
throw new BadRequestException(String.format(Locale.ROOT, msg.getBROKEN_MODEL_DESC(), modelDesc.getName()));
}
return modelDesc;
}
public DataModelDesc getModel(final String modelName, final String projectName) throws IOException {
if (null == projectName) {
aclEvaluate.checkIsGlobalAdmin();
} else {
aclEvaluate.checkProjectReadPermission(projectName);
}
return getDataModelManager().getDataModelDesc(modelName);
}
public Draft getModelDraft(String modelName, String projectName) throws IOException {
for (Draft d : listModelDrafts(modelName, projectName)) {
return d;
}
return null;
}
public List<Draft> listModelDrafts(String modelName, String projectName) throws IOException {
if (null == projectName) {
aclEvaluate.checkIsGlobalAdmin();
} else {
aclEvaluate.checkProjectReadPermission(projectName);
}
List<Draft> result = new ArrayList<>();
for (Draft d : getDraftManager().list(projectName)) {
RootPersistentEntity e = d.getEntity();
if (e instanceof DataModelDesc) {
DataModelDesc m = (DataModelDesc) e;
if (StringUtils.isEmpty(modelName) || modelName.equals(m.getName()))
result.add(d);
}
}
return result;
}
}