All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gr.uom.java.xmi.diff.ExtractOperationDetection Maven / Gradle / Ivy

Go to download

RefactoringMiner is a library/API written in Java that can detect refactorings applied in the history of a Java project.

There is a newer version: 3.0.9
Show newest version
package gr.uom.java.xmi.diff;

import static gr.uom.java.xmi.Constants.JAVA;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.refactoringminer.api.RefactoringMinerTimedOutException;

import gr.uom.java.xmi.UMLOperation;
import gr.uom.java.xmi.UMLParameter;
import gr.uom.java.xmi.VariableDeclarationContainer;
import gr.uom.java.xmi.LocationInfo.CodeElementType;
import gr.uom.java.xmi.decomposition.AbstractCall;
import gr.uom.java.xmi.decomposition.AbstractCodeFragment;
import gr.uom.java.xmi.decomposition.AbstractCodeMapping;
import gr.uom.java.xmi.decomposition.AbstractExpression;
import gr.uom.java.xmi.decomposition.CompositeStatementObject;
import gr.uom.java.xmi.decomposition.CompositeStatementObjectMapping;
import gr.uom.java.xmi.decomposition.LambdaExpressionObject;
import gr.uom.java.xmi.decomposition.LeafExpression;
import gr.uom.java.xmi.decomposition.OperationInvocation;
import gr.uom.java.xmi.decomposition.StatementObject;
import gr.uom.java.xmi.decomposition.UMLOperationBodyMapper;
import gr.uom.java.xmi.decomposition.VariableDeclaration;
import gr.uom.java.xmi.decomposition.replacement.Replacement;
import gr.uom.java.xmi.decomposition.replacement.Replacement.ReplacementType;

public class ExtractOperationDetection {
	private UMLOperationBodyMapper mapper;
	private List addedOperations;
	private UMLAbstractClassDiff classDiff;
	private UMLModelDiff modelDiff;
	private List operationInvocations;
	private Map callTreeMap = new LinkedHashMap();
	private Map> callCountMap = null;
	private List potentiallyMovedOperations = new ArrayList();

	public ExtractOperationDetection(UMLOperationBodyMapper mapper, List addedOperations, UMLAbstractClassDiff classDiff, UMLModelDiff modelDiff, boolean invocationsFromOtherMappers) {
		this.mapper = mapper;
		this.addedOperations = addedOperations;
		this.classDiff = classDiff;
		this.modelDiff = modelDiff;
		if(invocationsFromOtherMappers) {
			addInvocationsFromOtherMappers();
		}
		else {
			this.operationInvocations = getInvocationsInSourceOperationAfterExtractionExcludingInvocationsInExactlyMappedStatements(mapper);
		}
	}

	public ExtractOperationDetection(UMLOperationBodyMapper mapper, List addedOperations, UMLAbstractClassDiff classDiff, UMLModelDiff modelDiff) {
		this(mapper, addedOperations, classDiff, modelDiff, false);
	}

	public ExtractOperationDetection(UMLOperationBodyMapper mapper, List potentiallyMovedOperations, List addedOperations, UMLAbstractClassDiff classDiff, UMLModelDiff modelDiff) {
		this(mapper, addedOperations, classDiff, modelDiff);
		this.potentiallyMovedOperations = potentiallyMovedOperations;
	}

	private void addInvocationsFromOtherMappers() {
		this.operationInvocations = new ArrayList<>();
		for(UMLOperationBodyMapper mapper : classDiff.getOperationBodyMapperList()) {
			if(!mapper.equals(this.mapper)) {
				for(AbstractCodeFragment statement : mapper.getNonMappedLeavesT2()) {
					addStatementInvocations(operationInvocations, statement);
				}
				for(AbstractCodeMapping mapping : mapper.getMappings()) {
					if(!mapping.isExact()) {
						addStatementInvocations(operationInvocations, mapping.getFragment2());
					}
				}
			}
		}
	}

	public List getAddedOperationsSortedByCalls() {
		this.callCountMap = new LinkedHashMap<>();
		List sorted = new ArrayList<>();
		List counts = new ArrayList<>();
		for(UMLOperation addedOperation : addedOperations) {
			List matchingInvocations = matchingInvocations(addedOperation, operationInvocations, mapper.getContainer2());
			if(!matchingInvocations.isEmpty()) {
				callCountMap.put(addedOperation, matchingInvocations);
				int count = matchingInvocations.size();
				if(sorted.isEmpty()) {
					sorted.add(addedOperation);
					counts.add(count);
				}
				else {
					boolean inserted = false;
					for(int i=0; i counts.get(i)) {
							sorted.add(i, addedOperation);
							counts.add(i, count);
							inserted = true;
							break;
						}
					}
					if(!inserted) {
						sorted.add(counts.size(), addedOperation);
						counts.add(counts.size(), count);
					}
				}
			}
		}
		return sorted;
	}

	public List check(UMLOperation addedOperation) throws RefactoringMinerTimedOutException {
		List refactorings = new ArrayList();
		if(modelDiff != null && modelDiff.refactoringListContainsAnotherMoveRefactoringWithTheSameAddedOperation(addedOperation)) {
			return refactorings;
		}
		if(!mapper.getNonMappedLeavesT1().isEmpty() || !mapper.getNonMappedInnerNodesT1().isEmpty() ||
			!mapper.getReplacementsInvolvingMethodInvocation().isEmpty() || mapper.containsCompositeMappingWithoutReplacements()) {
			List addedOperationInvocations = callCountMap != null ? callCountMap.get(addedOperation) : matchingInvocations(addedOperation, operationInvocations, mapper.getContainer2());
			if(addedOperationInvocations.size() > 0) {
				int otherAddedMethodsCalled = 0;
				int otherAddedMethodsCalledWithSameOrMoreCallSites = 0;
				for(UMLOperation addedOperation2 : this.addedOperations) {
					if(!addedOperation.equals(addedOperation2)) {
						List addedOperationInvocations2 = callCountMap != null ? callCountMap.get(addedOperation2) : matchingInvocations(addedOperation2, operationInvocations, mapper.getContainer2());
						if(addedOperationInvocations2 != null && addedOperationInvocations2.size() > 0) {
							otherAddedMethodsCalled++;
						}
						if(addedOperationInvocations2 != null && addedOperationInvocations2.size() >= addedOperationInvocations.size()) {
							otherAddedMethodsCalledWithSameOrMoreCallSites++;
						}
					}
				}
				//check if the source method contains more statements than (addedOperationInvocations.size() * addedOperation statements)
				if(otherAddedMethodsCalledWithSameOrMoreCallSites == 0 && (otherAddedMethodsCalled == 0 || mapper.getContainer1().stringRepresentation().size() > addedOperationInvocations.size() * addedOperation.stringRepresentation().size())) {
					List sortedInvocations = sortInvocationsBasedOnArgumentOccurrences(addedOperationInvocations);
					for(AbstractCall addedOperationInvocation : sortedInvocations) {
						processAddedOperation(addedOperation, refactorings, sortedInvocations, addedOperationInvocation);
						if(sortedInvocations.size() == 1 && addedOperationInvocation.arguments().size() == 1) {
							String argument = addedOperationInvocation.arguments().get(0);
							Set declarations = mapper.getContainer2().variableDeclarationMap().get(argument);
							if(declarations != null && declarations.size() == 1) {
								VariableDeclaration declaration = declarations.iterator().next();
								if(declaration.getInitializer() != null && declaration.getInitializer().getTernaryOperatorExpressions().size() == 1) {
									processAddedOperation(addedOperation, refactorings, sortedInvocations, addedOperationInvocation);
								}
							}
						}
					}
				}
				else {
					processAddedOperation(addedOperation, refactorings, addedOperationInvocations, addedOperationInvocations.get(0));
				}
			}
		}
		return refactorings;
	}

	private List sortInvocationsBasedOnArgumentOccurrences(List invocations) {
		if(invocations.size() > 1) {
			List sorted = new ArrayList();
			List allVariables = new ArrayList();
			for(CompositeStatementObject composite : mapper.getNonMappedInnerNodesT1()) {
				for(LeafExpression expression : composite.getVariables()) {
					allVariables.add(expression.getString());
				}
			}
			for(AbstractCodeFragment leaf : mapper.getNonMappedLeavesT1()) {
				for(LeafExpression expression : leaf.getVariables()) {
					allVariables.add(expression.getString());
				}
			}
			int max = 0;
			for(AbstractCall invocation : invocations) {
				List arguments = invocation.arguments();
				int occurrences = 0;
				for(String argument : arguments) {
					if(argument.startsWith(JAVA.THIS_DOT) && !allVariables.contains(argument)) {
						String substringAfterThis = argument.substring(5);
						occurrences += Collections.frequency(allVariables, substringAfterThis);
					}
					else {
						occurrences += Collections.frequency(allVariables, argument);
					}
				}
				if(occurrences > max) {
					sorted.add(0, invocation);
					max = occurrences;
				}
				else {
					sorted.add(invocation);
				}
			}
			return sorted;
		}
		else {
			return invocations;
		}
	}

	private void processAddedOperation(UMLOperation addedOperation,
			List refactorings,
			List addedOperationInvocations, AbstractCall addedOperationInvocation)
			throws RefactoringMinerTimedOutException {
		CallTreeNode root = new CallTreeNode(mapper.getContainer1(), addedOperation, addedOperationInvocation);
		CallTree callTree = null;
		if(callTreeMap.containsKey(root)) {
			callTree = callTreeMap.get(root);
		}
		else {
			callTree = new CallTree(root);
			generateCallTree(addedOperation, root, callTree);
			callTreeMap.put(root, callTree);
		}
		UMLOperationBodyMapper operationBodyMapper = createMapperForExtractedMethod(mapper, mapper.getContainer1(), addedOperation, addedOperationInvocation, false);
		if(operationBodyMapper != null && !containsRefactoringWithIdenticalMappings(refactorings, operationBodyMapper)) {
			List additionalExactMatches = new ArrayList();
			List nodesInBreadthFirstOrder = callTree.getNodesInBreadthFirstOrder();
			for(int i=1; i refactorings, List additionalExactMatches,
			CallTreeNode node) throws RefactoringMinerTimedOutException {
		UMLOperationBodyMapper nestedMapper = createMapperForExtractedMethod(mapper, node.getOriginalOperation(), node.getInvokedOperation(), node.getInvocation(), true);
		if(nestedMapper != null && !containsRefactoringWithIdenticalMappings(refactorings, nestedMapper)) {
			additionalExactMatches.addAll(nestedMapper.getExactMatches());
			if(extractMatchCondition(nestedMapper, new ArrayList()) && (extractMatchCondition(operationBodyMapper, additionalExactMatches) || node.getOriginalOperation().delegatesTo(node.getInvokedOperation(), classDiff, modelDiff) != null)) {
				List nestedMatchingInvocations = matchingInvocations(node.getInvokedOperation(), node.getOriginalOperation().getAllOperationInvocations(), node.getOriginalOperation());
				ExtractOperationRefactoring nestedRefactoring = new ExtractOperationRefactoring(nestedMapper, mapper.getContainer2(), nestedMatchingInvocations);
				refactorings.add(nestedRefactoring);
				operationBodyMapper.addChildMapper(nestedMapper);
			}
			else {
				//add any mappings back to parent mapper as non-mapped statements
				for(AbstractCodeMapping mapping : nestedMapper.getMappings()) {
					AbstractCodeFragment fragment1 = mapping.getFragment1();
					if(fragment1 instanceof CompositeStatementObject) {
						if(!mapper.getNonMappedInnerNodesT1().contains(fragment1)) {
							mapper.getNonMappedInnerNodesT1().add((CompositeStatementObject)fragment1);
						}
					}
					else {
						if(!mapper.getNonMappedLeavesT1().contains(fragment1)) {
							mapper.getNonMappedLeavesT1().add(fragment1);
						}
					}
				}
			}
		}
	}

	private boolean containsRefactoringWithIdenticalMappings(List refactorings, UMLOperationBodyMapper mapper) {
		Set newMappings = mapper.getMappings();
		for(ExtractOperationRefactoring ref : refactorings) {
			Set oldMappings = ref.getBodyMapper().getMappings();
			if(oldMappings.containsAll(newMappings)) {
				return true;
			}
		}
		return false;
	}

	private static List getInvocationsInSourceOperationAfterExtractionExcludingInvocationsInExactlyMappedStatements(UMLOperationBodyMapper mapper) {
		List operationInvocations = mapper.getContainer2().getAllOperationInvocations();
		for(AbstractCodeMapping mapping : mapper.getMappings()) {
			if(mapping.isExact() && mapping.getReplacementsInvolvingMethodInvocation().isEmpty()) {
				for(AbstractCall invocation : mapping.getFragment2().getMethodInvocations()) {
					for(ListIterator iterator = operationInvocations.listIterator(); iterator.hasNext();) {
						AbstractCall matchingInvocation = iterator.next();
						if(invocation == matchingInvocation || invocation.actualString().equals(matchingInvocation.actualString())) {
							iterator.remove();
							break;
						}
					}
				}
			}
		}
		for(AbstractCodeFragment statement : mapper.getNonMappedLeavesT2()) {
			addStatementInvocations(operationInvocations, statement);
		}
		return operationInvocations;
	}

	public static List getInvocationsInSourceOperationAfterExtraction(UMLOperationBodyMapper mapper) {
		List operationInvocations = getInvocationsInSourceOperationAfterExtractionExcludingInvocationsInExactlyMappedStatements(mapper);
		if(operationInvocations.isEmpty()) {
			return operationInvocations;
		}
		List invocationsInSourceOperationBeforeExtraction = mapper.getContainer1().getAllOperationInvocations();
		for(AbstractCall invocation : invocationsInSourceOperationBeforeExtraction) {
			for(ListIterator iterator = operationInvocations.listIterator(); iterator.hasNext();) {
				AbstractCall matchingInvocation = iterator.next();
				if(invocation.getName().equals(matchingInvocation.getName())) {
					boolean matchingInvocationFound = false;
					for(String argument : invocation.arguments()) {
						if(argument.equals(matchingInvocation.getExpression())) {
							iterator.remove();
							matchingInvocationFound = true;
							break;
						}
					}
					if(matchingInvocationFound) {
						break;
					}
				}
			}
		}
		return operationInvocations;
	}

	public static void addStatementInvocations(List operationInvocations, AbstractCodeFragment statement) {
		for(AbstractCall statementInvocation : statement.getMethodInvocations()) {
			if(!containsInvocation(operationInvocations, statementInvocation)) {
				operationInvocations.add(statementInvocation);
			}
		}
		List lambdas = statement.getLambdas();
		for(LambdaExpressionObject lambda : lambdas) {
			for(AbstractCall statementInvocation : lambda.getAllOperationInvocations()) {
				if(!containsInvocation(operationInvocations, statementInvocation)) {
					operationInvocations.add(statementInvocation);
				}
			}
		}
	}

	public static boolean containsInvocation(List operationInvocations, AbstractCall invocation) {
		for(AbstractCall operationInvocation : operationInvocations) {
			if(operationInvocation.getLocationInfo().equals(invocation.getLocationInfo())) {
				return true;
			}
		}
		return false;
	}

	private List matchingInvocations(UMLOperation operation,
			List operationInvocations, VariableDeclarationContainer callerOperation) {
		List addedOperationInvocations = new ArrayList();
		for(AbstractCall invocation : operationInvocations) {
			if(invocation.matchesOperation(operation, callerOperation, classDiff, modelDiff)) {
				addedOperationInvocations.add(invocation);
			}
		}
		return addedOperationInvocations;
	}

	private void generateCallTree(UMLOperation operation, CallTreeNode parent, CallTree callTree) {
		List invocations = operation.getAllOperationInvocations();
		for(UMLOperation addedOperation : addedOperations) {
			for(AbstractCall invocation : invocations) {
				if(invocation.matchesOperation(addedOperation, operation, classDiff, modelDiff)) {
					if(!callTree.containsInPathToRootOrSibling(parent, addedOperation)) {
						CallTreeNode node = new CallTreeNode(parent, operation, addedOperation, invocation);
						parent.addChild(node);
						generateCallTree(addedOperation, node, callTree);
					}
				}
			}
		}
	}

	private UMLOperationBodyMapper createMapperForExtractedMethod(UMLOperationBodyMapper mapper,
			VariableDeclarationContainer originalOperation, UMLOperation addedOperation, AbstractCall addedOperationInvocation, boolean nested) throws RefactoringMinerTimedOutException {
		for(UMLOperation potentiallyMovedOperation : potentiallyMovedOperations) {
			if(potentiallyMovedOperation.equalSignature(addedOperation)) {
				return null;
			}
		}
		List originalMethodParameters = originalOperation.getParametersWithoutReturnType();
		Map originalMethodParametersPassedAsArgumentsMappedToCalledMethodParameters = new LinkedHashMap();
		List arguments = addedOperationInvocation.arguments();
		List parameters = addedOperation.getParametersWithoutReturnType();
		Map parameterToArgumentMap = new LinkedHashMap();
		//special handling for methods with varargs parameter for which no argument is passed in the matching invocation
		int size = Math.min(arguments.size(), parameters.size());
		for(int i=0; i(), parameterToArgumentMap, classDiff, addedOperationInvocation, nested);
		}
		return null;
	}

	private boolean extractMatchCondition(UMLOperationBodyMapper operationBodyMapper, List additionalExactMatches) {
		if(operationBodyMapper.getMappings().size() == 1) {
			AbstractCodeMapping mapping = operationBodyMapper.getMappings().iterator().next();
			if(mapping.getFragment1() instanceof AbstractExpression) {
				for(AbstractCodeMapping parentMapping : operationBodyMapper.getParentMapper().getMappings()) {
					if(parentMapping instanceof CompositeStatementObjectMapping) {
						CompositeStatementObject parentComp1 = (CompositeStatementObject) parentMapping.getFragment1();
						CompositeStatementObject parentComp2 = (CompositeStatementObject) parentMapping.getFragment2();
						if(parentComp1.getExpressions().contains(mapping.getFragment1()) &&
								!parentComp2.getMethodInvocations().contains(operationBodyMapper.getOperationInvocation())) {
							return false;
						}
					}
				}
			}
			if(operationBodyMapper.isNested() && operationBodyMapper.getParentMapper() != null) {
				if(operationBodyMapper.getParentMapper().getMappings().size() == 1 &&
						operationBodyMapper.getParentMapper().getContainer1().isDelegate() != null &&
						operationBodyMapper.getParentMapper().getContainer2().isDelegate() != null) {
					return false;
				}
			}
		}
		int mappings = operationBodyMapper.mappingsWithoutBlocks();
		int nonMappedElementsT1 = operationBodyMapper.nonMappedElementsT1();
		int nonMappedElementsT2 = operationBodyMapper.nonMappedElementsT2();
		List exactMatchList = new ArrayList(operationBodyMapper.getExactMatches());
		boolean exceptionHandlingExactMatch = false;
		boolean throwsNewExceptionExactMatch = false;
		if(exactMatchList.size() == 1) {
			AbstractCodeMapping mapping = exactMatchList.get(0);
			if(mapping.getFragment1() instanceof StatementObject && mapping.getFragment2() instanceof StatementObject) {
				StatementObject statement1 = (StatementObject)mapping.getFragment1();
				StatementObject statement2 = (StatementObject)mapping.getFragment2();
				if(statement1.getParent().getString().startsWith("catch(") &&
						statement2.getParent().getString().startsWith("catch(")) {
					exceptionHandlingExactMatch = true;
				}
			}
			if(mapping.getFragment1().throwsNewException() && mapping.getFragment2().throwsNewException()) {
				throwsNewExceptionExactMatch = true;
			}
		}
		for(AbstractCodeMapping mapping : operationBodyMapper.getMappings()) {
			List variableDeclarations = mapping.getFragment2().getVariableDeclarations();
			if(variableDeclarations.size() > 0) {
				for(VariableDeclaration variableDeclaration : variableDeclarations) {
					for(AbstractCodeFragment leaf2 : operationBodyMapper.getNonMappedLeavesT2()) {
						if(leaf2.countableStatement() && leaf2.getString().equals(JAVA.RETURN_SPACE + variableDeclaration.getVariableName() + JAVA.STATEMENT_TERMINATION)) {
							nonMappedElementsT2--;
							break;
						}
					}
	 			}
			}
		}
		if(nonMappedElementsT2 == 1) {
			for(AbstractCodeFragment fragment2 : operationBodyMapper.getNonMappedLeavesT2()) {
				List variableDeclarations = fragment2.getVariableDeclarations();
				if(variableDeclarations.size() > 0) {
					for(VariableDeclaration variableDeclaration : variableDeclarations) {
						for(AbstractCodeMapping mapping : operationBodyMapper.getMappings()) {
							boolean matchingReplacementFound = false;
							for(Replacement r : mapping.getReplacements()) {
								if(r.getAfter().equals(variableDeclaration.getVariableName())) {
									matchingReplacementFound = true;
								}
							}
							if(matchingReplacementFound) {
								nonMappedElementsT2--;
								break;
							}
						}
		 			}
				}
			}
		}
		exactMatchList.addAll(additionalExactMatches);
		int exactMatches = exactMatchList.size();
		if(exactMatches == 0 && operationBodyMapper.getMappings().size() >= 1 && operationBodyMapper.getMappings().size() <= 2) {
			int beforeAfterContains = 0;
			AbstractCodeMapping mapping = operationBodyMapper.getMappings().iterator().next();
			for(Replacement r : mapping.getReplacements()) {
				if(r.getAfter().contains(r.getBefore()) || r.getBefore().contains(r.getAfter())) {
					beforeAfterContains++;
				}
			}
			if(beforeAfterContains == mapping.getReplacements().size()) {
				exactMatches++;
			}
		}
		return mappings > 0 && (mappings > nonMappedElementsT2 || (mappings > 1 && mappings >= nonMappedElementsT2) ||
				(exactMatches >= mappings && nonMappedElementsT1 == 0) ||
				(exactMatches == 1 && !throwsNewExceptionExactMatch && nonMappedElementsT2-exactMatches <= 10) ||
				(!exceptionHandlingExactMatch && exactMatches > 1 && additionalExactMatches.size() < exactMatches && nonMappedElementsT2-exactMatches < 20) ||
				(mappings == 1 && mappings > operationBodyMapper.nonMappedLeafElementsT2())) ||
				argumentExtractedWithDefaultReturnAdded(operationBodyMapper);
	}

	private boolean argumentExtractedWithDefaultReturnAdded(UMLOperationBodyMapper operationBodyMapper) {
		List totalMappings = new ArrayList(operationBodyMapper.getMappings());
		List nonMappedInnerNodesT2 = new ArrayList(operationBodyMapper.getNonMappedInnerNodesT2());
		ListIterator iterator = nonMappedInnerNodesT2.listIterator();
		while(iterator.hasNext()) {
			if(iterator.next().getString().equals(JAVA.OPEN_BLOCK)) {
				iterator.remove();
			}
		}
		List nonMappedLeavesT2 = operationBodyMapper.getNonMappedLeavesT2();
		return totalMappings.size() == 1 && totalMappings.get(0).containsReplacement(ReplacementType.ARGUMENT_REPLACED_WITH_RETURN_EXPRESSION) &&
				nonMappedInnerNodesT2.size() == 1 && nonMappedInnerNodesT2.get(0).toString().startsWith("if") &&
				nonMappedLeavesT2.size() == 1 && nonMappedLeavesT2.get(0).toString().startsWith(JAVA.RETURN_SPACE);
	}

	private UMLOperation findDelegateMethod(VariableDeclarationContainer originalOperation, UMLOperation addedOperation, AbstractCall addedOperationInvocation) {
		AbstractCall delegateMethodInvocation = addedOperation.isDelegate();
		if(originalOperation.isDelegate() == null && delegateMethodInvocation != null && !originalOperation.getAllOperationInvocations().contains(addedOperationInvocation)) {
			for(UMLOperation operation : addedOperations) {
				if(delegateMethodInvocation.matchesOperation(operation, addedOperation, classDiff, modelDiff)) {
					return operation;
				}
			}
		}
		return null;
	}

	private boolean parameterTypesMatch(Map originalMethodParametersPassedAsArgumentsMappedToCalledMethodParameters) {
		for(UMLParameter key : originalMethodParametersPassedAsArgumentsMappedToCalledMethodParameters.keySet()) {
			UMLParameter value = originalMethodParametersPassedAsArgumentsMappedToCalledMethodParameters.get(key);
			if(!key.getType().equals(value.getType()) && !key.getType().equalsWithSubType(value.getType()) && !key.getType().equalClassType(value.getType()) &&
					!OperationInvocation.compatibleTypes(value, key.getType(), classDiff, modelDiff)) {
				return false;
			}
		}
		return true;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy