/*
* FindBugs - Find Bugs in Java programs
* Copyright (C) 2003-2008 University of Maryland
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.umd.cs.findbugs.detect;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.annotation.meta.When;
import org.apache.bcel.classfile.Method;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.LocalVariableAnnotation;
import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.StringAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.BasicBlock;
import edu.umd.cs.findbugs.ba.CFG;
import edu.umd.cs.findbugs.ba.DataflowAnalysisException;
import edu.umd.cs.findbugs.ba.DataflowCFGPrinter;
import edu.umd.cs.findbugs.ba.Edge;
import edu.umd.cs.findbugs.ba.EdgeTypes;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.ba.jsr305.Analysis;
import edu.umd.cs.findbugs.ba.jsr305.BackwardTypeQualifierDataflow;
import edu.umd.cs.findbugs.ba.jsr305.BackwardTypeQualifierDataflowAnalysis;
import edu.umd.cs.findbugs.ba.jsr305.BackwardTypeQualifierDataflowFactory;
import edu.umd.cs.findbugs.ba.jsr305.FlowValue;
import edu.umd.cs.findbugs.ba.jsr305.ForwardTypeQualifierDataflow;
import edu.umd.cs.findbugs.ba.jsr305.ForwardTypeQualifierDataflowAnalysis;
import edu.umd.cs.findbugs.ba.jsr305.ForwardTypeQualifierDataflowFactory;
import edu.umd.cs.findbugs.ba.jsr305.SourceSinkInfo;
import edu.umd.cs.findbugs.ba.jsr305.SourceSinkType;
import edu.umd.cs.findbugs.ba.jsr305.TypeQualifierAnnotation;
import edu.umd.cs.findbugs.ba.jsr305.TypeQualifierApplications;
import edu.umd.cs.findbugs.ba.jsr305.TypeQualifierValue;
import edu.umd.cs.findbugs.ba.jsr305.TypeQualifierValueSet;
import edu.umd.cs.findbugs.ba.vna.ValueNumber;
import edu.umd.cs.findbugs.ba.vna.ValueNumberDataflow;
import edu.umd.cs.findbugs.ba.vna.ValueNumberFrame;
import edu.umd.cs.findbugs.ba.vna.ValueNumberSourceInfo;
import edu.umd.cs.findbugs.bcel.CFGDetector;
import edu.umd.cs.findbugs.classfile.CheckedAnalysisException;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.Global;
import edu.umd.cs.findbugs.classfile.IAnalysisCache;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.classfile.MissingClassException;
import edu.umd.cs.findbugs.util.Util;
/**
* Check JSR-305 type qualifiers.
*
* @author David Hovemeyer
*/
public class CheckTypeQualifiers extends CFGDetector {
private static final boolean DEBUG = SystemProperties.getBoolean("ctq.debug");
private static final boolean DEBUG_DATAFLOW = SystemProperties.getBoolean("ctq.dataflow.debug");
private static final String DEBUG_DATAFLOW_MODE = SystemProperties.getProperty("ctq.dataflow.debug.mode", "both");
private static final String NONNULL_ANNOTATION = "javax/annotation/Nonnull";
private static final String METHOD = SystemProperties.getProperty("ctq.method");
private final BugReporter bugReporter;
public CheckTypeQualifiers(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
boolean checked;
boolean shouldRunAnalysis;
/* (non-Javadoc)
* @see edu.umd.cs.findbugs.bcel.CFGDetector#visitClass(edu.umd.cs.findbugs.classfile.ClassDescriptor)
*/
@Override
public void visitClass(ClassDescriptor classDescriptor) throws CheckedAnalysisException {
if (!checked) {
checked = true;
Collection<TypeQualifierValue> allKnownTypeQualifiers = TypeQualifierValue.getAllKnownTypeQualifiers();
int size = allKnownTypeQualifiers.size();
if (size == 1) {
TypeQualifierValue value = Util.first(allKnownTypeQualifiers);
if (!value.typeQualifier.getClassName().equals(NONNULL_ANNOTATION))
shouldRunAnalysis = true;
}
else if (size > 1)
shouldRunAnalysis = true;
}
if (shouldRunAnalysis)
super.visitClass(classDescriptor);
}
/* (non-Javadoc)
* @see edu.umd.cs.findbugs.bcel.CFGDetector#visitMethodCFG(edu.umd.cs.findbugs.classfile.MethodDescriptor, edu.umd.cs.findbugs.ba.CFG)
*/
@Override
protected void visitMethodCFG(MethodDescriptor methodDescriptor, CFG cfg) throws CheckedAnalysisException {
if (METHOD != null && !methodDescriptor.getName().equals(METHOD)) {
return;
}
if (DEBUG) {
System.out.println("CheckTypeQualifiers: checking " + methodDescriptor.toString());
}
IAnalysisCache analysisCache = Global.getAnalysisCache();
ForwardTypeQualifierDataflowFactory forwardDataflowFactory =
analysisCache.getMethodAnalysis(ForwardTypeQualifierDataflowFactory.class, methodDescriptor);
BackwardTypeQualifierDataflowFactory backwardDataflowFactory =
analysisCache.getMethodAnalysis(BackwardTypeQualifierDataflowFactory.class, methodDescriptor);
ValueNumberDataflow vnaDataflow =
analysisCache.getMethodAnalysis(ValueNumberDataflow.class, methodDescriptor);
Collection<TypeQualifierValue> relevantQualifiers = Analysis.getRelevantTypeQualifiers(methodDescriptor, cfg);
if (DEBUG) {
System.out.println(" Relevant type qualifiers are " + relevantQualifiers);
}
for (TypeQualifierValue typeQualifierValue : relevantQualifiers) {
if (typeQualifierValue.getTypeQualifierClassDescriptor().getClassName().equals(NONNULL_ANNOTATION)) {
// Checking @Nonnull annotations is the bailiwick of FindNullDeref.
continue;
}
try {
checkQualifier(
methodDescriptor,
cfg,
typeQualifierValue,
forwardDataflowFactory,
backwardDataflowFactory,
vnaDataflow
);
} catch (MissingClassException e) {
AnalysisContext.reportMissingClass(e);
} catch (CheckedAnalysisException e) {
bugReporter.logError(
"Exception checking type qualifier " + typeQualifierValue.toString() +
" on method " + methodDescriptor.toString(),
e);
}
}
}
private String checkLocation;
/**
* Check a specific TypeQualifierValue on a method.
*
* @param methodDescriptor MethodDescriptor of method
* @param cfg CFG of method
* @param typeQualifierValue TypeQualifierValue to check
* @param forwardDataflowFactory ForwardTypeQualifierDataflowFactory used to create forward dataflow analysis objects
* @param backwardDataflowFactory BackwardTypeQualifierDataflowFactory used to create backward dataflow analysis objects
* @param vnaDataflow ValueNumberDataflow for the method
*/
private void checkQualifier(
MethodDescriptor methodDescriptor,
CFG cfg,
TypeQualifierValue typeQualifierValue,
ForwardTypeQualifierDataflowFactory forwardDataflowFactory,
BackwardTypeQualifierDataflowFactory backwardDataflowFactory,
ValueNumberDataflow vnaDataflow) throws CheckedAnalysisException {
if (DEBUG) {
System.out.println("----------------------------------------------------------------------");
System.out.println("Checking type qualifier " + typeQualifierValue.toString() + " on method " + methodDescriptor.toString());
if (typeQualifierValue.isStrictQualifier()) {
System.out.println(" Strict type qualifier");
}
System.out.println("----------------------------------------------------------------------");
}
ForwardTypeQualifierDataflow forwardDataflow = forwardDataflowFactory.getDataflow(typeQualifierValue);
if (DEBUG_DATAFLOW && (DEBUG_DATAFLOW_MODE.startsWith("forward") || DEBUG_DATAFLOW_MODE.equals("both"))) {
System.out.println("********* Forwards analysis *********");
DataflowCFGPrinter<TypeQualifierValueSet, ForwardTypeQualifierDataflowAnalysis> p =
new DataflowCFGPrinter<TypeQualifierValueSet, ForwardTypeQualifierDataflowAnalysis>(forwardDataflow);
p.print(System.out);
}
BackwardTypeQualifierDataflow backwardDataflow = backwardDataflowFactory.getDataflow(typeQualifierValue);
if (DEBUG_DATAFLOW && (DEBUG_DATAFLOW_MODE.startsWith("backward") || DEBUG_DATAFLOW_MODE.equals("both"))) {
System.out.println("********* Backwards analysis *********");
DataflowCFGPrinter<TypeQualifierValueSet, BackwardTypeQualifierDataflowAnalysis> p =
new DataflowCFGPrinter<TypeQualifierValueSet, BackwardTypeQualifierDataflowAnalysis>(backwardDataflow);
p.print(System.out);
}
checkDataflow(methodDescriptor, cfg, typeQualifierValue, vnaDataflow, forwardDataflow, backwardDataflow);
checkValueSources(methodDescriptor, cfg, typeQualifierValue, vnaDataflow, forwardDataflow, backwardDataflow);
}
private void checkDataflow(MethodDescriptor methodDescriptor, CFG cfg, TypeQualifierValue typeQualifierValue,
ValueNumberDataflow vnaDataflow, ForwardTypeQualifierDataflow forwardDataflow,
BackwardTypeQualifierDataflow backwardDataflow) throws DataflowAnalysisException, CheckedAnalysisException {
for (Iterator<Location> i = cfg.locationIterator(); i.hasNext();) {
Location loc = i.next();
TypeQualifierValueSet forwardsFact = forwardDataflow.getFactAtLocation(loc);
TypeQualifierValueSet backwardsFact = backwardDataflow.getFactAfterLocation(loc);
if (!forwardsFact.isValid() || !backwardsFact.isValid()) {
continue;
}
if (DEBUG) {
checkLocation = "location " + loc.toCompactString();
}
checkForConflictingValues(
methodDescriptor,
typeQualifierValue,
forwardsFact,
backwardsFact,
loc, // location to report
loc, // location where doomed value is observed
vnaDataflow.getFactAtLocation(loc)
);
}
for (Iterator<Edge> i = cfg.edgeIterator(); i.hasNext(); ) {
Edge edge = i.next();
if (DEBUG) {
checkLocation = "edge " + edge.getLabel();
System.out.println("BEGIN CHECK EDGE " + edge.getLabel());
}
// NOTE: when checking forwards and backwards values on an edge,
// we don't want to apply BOTH edge transfer functions,
// since the purpose of the edge transfer function is to
// propagate information across phi nodes (effectively
// copying information about one value to another).
// Due to pruning of backwards values when a conflict is detected,
// we need to check backwards values as "early" as possible,
// meaning that we want to check at the edge target
// (before the backwards edge transfer function has pruned
// the backwards value.)
TypeQualifierValueSet forwardFact = forwardDataflow.getFactOnEdge(edge);
TypeQualifierValueSet backwardFact = backwardDataflow.getResultFact(edge.getTarget());
// The edge target location is where we can check
// for conflicting flow values.
Location edgeTargetLocation = getEdgeTargetLocation(cfg, edge);
ValueNumberFrame vnaFrame = (edgeTargetLocation != null) ? vnaDataflow.getFactAtLocation(edgeTargetLocation) : null;
// What location do we want to report to the user
// as where the conflict occurs?
// The edge source location is generally better,
// but edge target location is ok as a fallback.
Location locationToReport;
if (edge.getSource().getLastInstruction() != null) {
locationToReport = getEdgeSourceLocation(cfg, edge);
} else {
locationToReport = edgeTargetLocation;
}
checkForConflictingValues(
methodDescriptor,
typeQualifierValue,
forwardFact,
backwardFact,
locationToReport,
edgeTargetLocation,
vnaFrame
);
if (DEBUG) {
System.out.println("END CHECK EDGE");
}
}
}
private void checkValueSources(MethodDescriptor methodDescriptor, CFG cfg, TypeQualifierValue typeQualifierValue,
ValueNumberDataflow vnaDataflow, ForwardTypeQualifierDataflow forwardDataflow,
BackwardTypeQualifierDataflow backwardDataflow) throws DataflowAnalysisException {
// Check to see if any backwards ALWAYS or NEVER values
// reach incompatible sources.
for (Iterator<Location> i = cfg.locationIterator(); i.hasNext(); ) {
Location location = i.next();
Set<SourceSinkInfo> sourceSet = forwardDataflow.getAnalysis().getSourceSinkInfoSet(location);
for (SourceSinkInfo source : sourceSet) {
ValueNumber vn = source.getValueNumber();
TypeQualifierValueSet backwardsFact = backwardDataflow.getFactAtLocation(location);
FlowValue backwardsFlowValue = backwardsFact.getValue(vn);
if (DEBUG) {
System.out.println("Checking value source at " + location.toCompactString());
System.out.println(" back=" + backwardsFact);
System.out.println(" source=" + source);
}
if (!(backwardsFlowValue == FlowValue.ALWAYS || backwardsFlowValue == FlowValue.NEVER)) {
continue;
}
// Check to see if this warning has already been reported because
// the dataflow values conflict directly with each other.
TypeQualifierValueSet forwardsFact = forwardDataflow.getFactAfterLocation(location);
FlowValue forwardsFlowValue = forwardsFact.getValue(vn);
if (FlowValue.valuesConflict(forwardsFlowValue, backwardsFlowValue)) {
continue;
}
if (FlowValue.backwardsValueConflictsWithSource(backwardsFlowValue, source, typeQualifierValue)) {
String bugType =
(backwardsFlowValue == FlowValue.NEVER) ? "TQ_MAYBE_SOURCE_VALUE_REACHES_NEVER_SINK" : "TQ_MAYBE_SOURCE_VALUE_REACHES_ALWAYS_SINK";
emitSourceWarning(
bugType,
methodDescriptor,
typeQualifierValue,
backwardsFlowValue,
backwardsFact,
source,
vn, location
);
} else if (source.getWhen() == When.UNKNOWN && source.getType() == SourceSinkType.PARAMETER) {
XMethod xmethod = XFactory.createXMethod(methodDescriptor);
int p = source.getParameter();
TypeQualifierAnnotation directTypeQualifierAnnotation = TypeQualifierApplications.getDirectTypeQualifierAnnotation(xmethod, p, typeQualifierValue);
if (directTypeQualifierAnnotation != null && directTypeQualifierAnnotation.when == When.UNKNOWN) {
String bugType =
(backwardsFlowValue == FlowValue.NEVER) ? "TQ_EXPLICIT_UNKNOWN_SOURCE_VALUE_REACHES_NEVER_SINK" : "TQ_EXPLICIT_UNKNOWN_SOURCE_VALUE_REACHES_ALWAYS_SINK";
emitSourceWarning(
bugType,
methodDescriptor,
typeQualifierValue,
backwardsFlowValue,
backwardsFact,
source,
vn, location
);
}
}}
}
}
private Location getEdgeTargetLocation(CFG cfg, Edge edge) {
BasicBlock targetBlock = edge.getTarget();
// Target block is nonempty?
if (targetBlock.getFirstInstruction() != null) {
return new Location(targetBlock.getFirstInstruction(), targetBlock);
}
// Target block is an ETB?
if (targetBlock.isExceptionThrower()) {
BasicBlock fallThroughSuccessor = cfg.getSuccessorWithEdgeType(targetBlock, EdgeTypes.FALL_THROUGH_EDGE);
if (fallThroughSuccessor == null) {
// Fall through edge might have been pruned
for (Iterator<Edge> i = cfg.removedEdgeIterator(); i.hasNext(); ) {
Edge removedEdge = i.next();
if (removedEdge.getSource() == targetBlock && removedEdge.getType() == EdgeTypes.FALL_THROUGH_EDGE) {
fallThroughSuccessor = removedEdge.getTarget();
break;
}
}
}
if (fallThroughSuccessor != null && fallThroughSuccessor.getFirstInstruction() != null) {
return new Location(fallThroughSuccessor.getFirstInstruction(), fallThroughSuccessor);
}
}
return null;
}
private Location getEdgeSourceLocation(CFG cfg, Edge edge) {
BasicBlock sourceBlock = edge.getSource();
return (sourceBlock.getLastInstruction() != null) ? new Location(sourceBlock.getLastInstruction(), sourceBlock) : null;
}
private void checkForConflictingValues(
MethodDescriptor methodDescriptor,
TypeQualifierValue typeQualifierValue,
TypeQualifierValueSet forwardsFact,
TypeQualifierValueSet backwardsFact,
Location locationToReport,
Location locationWhereDoomedValueIsObserved,
ValueNumberFrame vnaFrame) throws CheckedAnalysisException {
Set<ValueNumber> valueNumberSet = new HashSet<ValueNumber>();
valueNumberSet.addAll(forwardsFact.getValueNumbers());
valueNumberSet.addAll(backwardsFact.getValueNumbers());
for (ValueNumber vn : valueNumberSet) {
FlowValue forward = forwardsFact.getValue(vn);
FlowValue backward = backwardsFact.getValue(vn);
if (DEBUG) {
System.out.println("Check " + vn + ": forward=" + forward + ", backward=" + backward + " at " + checkLocation);
}
if (FlowValue.valuesConflict(forward, backward)) {
if (DEBUG) {
System.out.println("Emitting warning at " + checkLocation);
}
emitDataflowWarning(
methodDescriptor,
typeQualifierValue,
forwardsFact,
backwardsFact,
vn,
forward,
backward,
locationToReport,
locationWhereDoomedValueIsObserved,
vnaFrame);
}
}
}
private void emitDataflowWarning(
MethodDescriptor methodDescriptor,
TypeQualifierValue typeQualifierValue,
TypeQualifierValueSet forwardsFact,
TypeQualifierValueSet backwardsFact,
ValueNumber vn,
FlowValue forward,
FlowValue backward,
Location locationToReport,
Location locationWhereDoomedValueIsObserved,
ValueNumberFrame vnaFrame) throws CheckedAnalysisException {
String bugType =
(backward == FlowValue.NEVER) ? "TQ_ALWAYS_VALUE_USED_WHERE_NEVER_REQUIRED" : "TQ_NEVER_VALUE_USED_WHERE_ALWAYS_REQUIRED";
// Issue warning
BugInstance warning = new BugInstance(this, bugType, Priorities.NORMAL_PRIORITY)
.addClassAndMethod(methodDescriptor);
annotateWarningWithTypeQualifier(warning, typeQualifierValue);
// Hopefully we can find the conflicted value in a local variable
if (locationWhereDoomedValueIsObserved != null) {
Method method = Global.getAnalysisCache().getMethodAnalysis(Method.class, methodDescriptor);
LocalVariableAnnotation localVariable =
ValueNumberSourceInfo.findLocalAnnotationFromValueNumber(method, locationWhereDoomedValueIsObserved, vn, vnaFrame);
if (localVariable != null) {
localVariable.setDescription(localVariable.isSignificant() ? "LOCAL_VARIABLE_VALUE_DOOMED_NAMED" : "LOCAL_VARIABLE_VALUE_DOOMED");
warning.add(localVariable);
}
// Report where we observed the value.
// Note that for conflicts detected on control edges,
// we REPORT the edge source location
// rather than the target location, even though it is the
// target location where the conflict is detected.
// The only reason to use a different reporting location
// is to produce a more informative report for the user,
// since the edge source is where the branch is found.
SourceLineAnnotation observedLocation = SourceLineAnnotation.fromVisitedInstruction(methodDescriptor, locationToReport);
observedLocation.setDescription("SOURCE_LINE_VALUE_DOOMED");
warning.add(observedLocation);
}
/*
// Add value sources
Set<SourceSinkInfo> sourceSet = (forward == FlowValue.ALWAYS) ? forwardsFact.getWhereAlways(vn) : forwardsFact.getWhereNever(vn);
for (SourceSinkInfo source : sourceSet) {
annotateWarningWithSourceSinkInfo(warning, methodDescriptor, vn, source);
}
*/
// Add value sinks
Set<SourceSinkInfo> sinkSet = (backward == FlowValue.ALWAYS) ? backwardsFact.getWhereAlways(vn) : backwardsFact.getWhereNever(vn);
for (SourceSinkInfo sink : sinkSet) {
annotateWarningWithSourceSinkInfo(warning, methodDescriptor, vn, sink);
}
bugReporter.reportBug(warning);
}
private void emitSourceWarning(
String bugType,
MethodDescriptor methodDescriptor,
TypeQualifierValue typeQualifierValue,
FlowValue backwardsFlowValue,
TypeQualifierValueSet backwardsFact,
SourceSinkInfo source,
ValueNumber vn, Location location) {
BugInstance warning = new BugInstance(this, bugType, Priorities.NORMAL_PRIORITY)
.addClassAndMethod(methodDescriptor);
annotateWarningWithTypeQualifier(warning, typeQualifierValue);
annotateWarningWithSourceSinkInfo(warning, methodDescriptor, vn, source);
Set<SourceSinkInfo> sinkSet = (backwardsFlowValue == FlowValue.NEVER) ? backwardsFact.getWhereNever(vn) : backwardsFact.getWhereAlways(vn);
for (SourceSinkInfo sink : sinkSet) {
annotateWarningWithSourceSinkInfo(warning, methodDescriptor, vn, sink);
}
bugReporter.reportBug(warning);
}
private void annotateWarningWithTypeQualifier(BugInstance warning, TypeQualifierValue typeQualifierValue) {
StringBuilder buf = new StringBuilder();
buf.append("@");
buf.append(typeQualifierValue.typeQualifier.getDottedClassName());
if (TypeQualifierValue.hasMultipleVariants(typeQualifierValue)) {
// When there are multiple variants, qualify the type
// qualifier with the value indicating which variant.
buf.append("(");
buf.append(typeQualifierValue.value);
buf.append(")");
}
warning.addString(buf.toString()).describe(StringAnnotation.TYPE_QUALIFIER_ROLE);
}
private void annotateWarningWithSourceSinkInfo(BugInstance warning, MethodDescriptor methodDescriptor, ValueNumber vn, SourceSinkInfo sourceSinkInfo) {
switch (sourceSinkInfo.getType()) {
case PARAMETER:
try {
Method method = Global.getAnalysisCache().getMethodAnalysis(Method.class, methodDescriptor);
LocalVariableAnnotation lva = LocalVariableAnnotation.getParameterLocalVariableAnnotation(
method,
sourceSinkInfo.getLocal());
lva.setDescription(lva.isSignificant()
? "LOCAL_VARIABLE_PARAMETER_VALUE_SOURCE_NAMED" : "LOCAL_VARIABLE_PARAMETER_VALUE_SOURCE");
warning.add(lva);
} catch (CheckedAnalysisException e) {
warning.addSourceLine(methodDescriptor, sourceSinkInfo.getLocation()).describe("SOURCE_LINE_VALUE_SOURCE");
}
break;
case RETURN_VALUE_OF_CALLED_METHOD:
case FIELD_LOAD:
warning.addSourceLine(methodDescriptor, sourceSinkInfo.getLocation()).describe("SOURCE_LINE_VALUE_SOURCE");
break;
case ARGUMENT_TO_CALLED_METHOD:
case RETURN_VALUE:
case FIELD_STORE:
warning.addSourceLine(methodDescriptor, sourceSinkInfo.getLocation()).describe("SOURCE_LINE_VALUE_SINK");
return;
default:
throw new IllegalStateException();
}
}
}