// projection.cpp
/* Copyright 2009 10gen Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects
* for all of the code used other than as permitted herein. If you modify
* file(s) with this exception, you may extend this exception to your
* version of the file(s), but you are not obligated to do so. If you do not
* wish to do so, delete this exception statement from your version. If you
* delete this exception statement from all source files in the program,
* then also delete it in the license file.
*/
#include "mongo/platform/basic.h"
#include "mongo/db/projection.h"
#include "mongo/db/matcher/matcher.h"
#include "mongo/util/log.h"
#include "mongo/util/mongoutils/str.h"
namespace mongo {
MONGO_LOG_DEFAULT_COMPONENT_FILE(::mongo::logger::LogComponent::kQuery);
void Projection::init(const BSONObj& o,
const MatchExpressionParser::WhereCallback& whereCallback) {
massert( 10371 , "can only add to Projection once", _source.isEmpty());
_source = o;
BSONObjIterator i( o );
int true_false = -1;
while ( i.more() ) {
BSONElement e = i.next();
if ( ! e.isNumber() )
_hasNonSimple = true;
if (e.type() == Object) {
BSONObj obj = e.embeddedObject();
BSONElement e2 = obj.firstElement();
if ( mongoutils::str::equals( e2.fieldName(), "$slice" ) ) {
if (e2.isNumber()) {
int i = e2.numberInt();
if (i < 0)
add(e.fieldName(), i, -i); // limit is now positive
else
add(e.fieldName(), 0, i);
}
else if (e2.type() == Array) {
BSONObj arr = e2.embeddedObject();
uassert(13099, "$slice array wrong size", arr.nFields() == 2 );
BSONObjIterator it(arr);
int skip = it.next().numberInt();
int limit = it.next().numberInt();
uassert(13100, "$slice limit must be positive", limit > 0 );
add(e.fieldName(), skip, limit);
}
else {
uassert(13098, "$slice only supports numbers and [skip, limit] arrays", false);
}
}
else if ( mongoutils::str::equals( e2.fieldName(), "$elemMatch" ) ) {
// validate $elemMatch arguments and dependencies
uassert( 16342, "elemMatch: invalid argument. object required.",
e2.type() == Object );
uassert( 16343, "Cannot specify positional operator and $elemMatch"
" (currently unsupported).",
_arrayOpType != ARRAY_OP_POSITIONAL );
uassert( 16344, "Cannot use $elemMatch projection on a nested field"
" (currently unsupported).",
! mongoutils::str::contains( e.fieldName(), '.' ) );
_arrayOpType = ARRAY_OP_ELEM_MATCH;
// initialize new Matcher object(s)
_matchers[mongoutils::str::before(e.fieldName(), '.').c_str()]
= boost::make_shared(e.wrap(), whereCallback);
add( e.fieldName(), true );
}
else {
uasserted(13097, string("Unsupported projection option: ") +
obj.firstElementFieldName() );
}
}
else if (!strcmp(e.fieldName(), "_id") && !e.trueValue()) {
_includeID = false;
}
else {
add( e.fieldName(), e.trueValue() );
// validate input
if (true_false == -1) {
true_false = e.trueValue();
_include = !e.trueValue();
}
else {
uassert( 10053 , "You cannot currently mix including and excluding fields. "
"Contact us if this is an issue." ,
(bool)true_false == e.trueValue() );
}
}
if ( mongoutils::str::contains( e.fieldName(), ".$" ) ) {
// positional op found; verify dependencies
uassert( 16345, "Cannot exclude array elements with the positional operator"
" (currently unsupported).", e.trueValue() );
uassert( 16346, "Cannot specify more than one positional array element per query"
" (currently unsupported).", _arrayOpType != ARRAY_OP_POSITIONAL );
uassert( 16347, "Cannot specify positional operator and $elemMatch"
" (currently unsupported).", _arrayOpType != ARRAY_OP_ELEM_MATCH );
_arrayOpType = ARRAY_OP_POSITIONAL;
}
}
}
void Projection::add(const string& field, bool include) {
if (field.empty()) { // this is the field the user referred to
_include = include;
}
else {
_include = !include;
const size_t dot = field.find('.');
const string subfield = field.substr(0,dot);
const string rest = (dot == string::npos ? "" : field.substr(dot+1,string::npos));
boost::shared_ptr& fm = _fields[subfield.c_str()];
if (!fm)
fm = boost::make_shared();
fm->add(rest, include);
}
}
void Projection::add(const string& field, int skip, int limit) {
_special = true; // can't include or exclude whole object
if (field.empty()) { // this is the field the user referred to
_skip = skip;
_limit = limit;
}
else {
const size_t dot = field.find('.');
const string subfield = field.substr(0,dot);
const string rest = (dot == string::npos ? "" : field.substr(dot+1,string::npos));
boost::shared_ptr& fm = _fields[subfield.c_str()];
if (!fm)
fm = boost::make_shared();
fm->add(rest, skip, limit);
}
}
void Projection::transform( const BSONObj& in , BSONObjBuilder& b, const MatchDetails* details ) const {
const ArrayOpType& arrayOpType = getArrayOpType();
BSONObjIterator i(in);
while ( i.more() ) {
BSONElement e = i.next();
if ( mongoutils::str::equals( "_id" , e.fieldName() ) ) {
if ( _includeID )
b.append( e );
}
else {
Matchers::const_iterator matcher = _matchers.find( e.fieldName() );
if ( matcher == _matchers.end() ) {
// no array projection matchers for this field
append( b, e, details, arrayOpType );
} else {
// field has array projection with $elemMatch specified.
massert( 16348, "matchers are only supported for $elemMatch",
arrayOpType == ARRAY_OP_ELEM_MATCH );
MatchDetails arrayDetails;
arrayDetails.requestElemMatchKey();
if ( matcher->second->matches( in, &arrayDetails ) ) {
LOG(4) << "Matched array on field: " << matcher->first << endl
<< " from array: " << in.getField( matcher->first ) << endl
<< " in object: " << in << endl
<< " at position: " << arrayDetails.elemMatchKey() << endl;
FieldMap::const_iterator field = _fields.find( e.fieldName() );
massert( 16349, "$elemMatch specified, but projection field not found.",
field != _fields.end() );
BSONArrayBuilder a;
BSONObjBuilder o;
massert( 16350, "$elemMatch called on document element with eoo",
! in.getField( e.fieldName() ).eoo() );
massert( 16351, "$elemMatch called on array element with eoo",
! in.getField( e.fieldName() ).Obj().getField(
arrayDetails.elemMatchKey() ).eoo() );
a.append( in.getField( e.fieldName() ).Obj()
.getField( arrayDetails.elemMatchKey() ) );
o.appendArray( matcher->first, a.arr() );
append( b, o.done().firstElement(), details, arrayOpType );
}
}
}
}
}
BSONObj Projection::transform( const BSONObj& in, const MatchDetails* details ) const {
BSONObjBuilder b;
transform( in , b, details );
return b.obj();
}
//b will be the value part of an array-typed BSONElement
void Projection::appendArray( BSONObjBuilder& b , const BSONObj& a , bool nested) const {
int skip = nested ? 0 : _skip;
int limit = nested ? -1 : _limit;
if (skip < 0) {
skip = max(0, skip + a.nFields());
}
int i=0;
BSONObjIterator it(a);
while (it.more()) {
BSONElement e = it.next();
if (skip) {
skip--;
continue;
}
if (limit != -1 && (limit-- == 0)) {
break;
}
switch(e.type()) {
case Array: {
BSONObjBuilder subb;
appendArray(subb , e.embeddedObject(), true);
b.appendArray(b.numStr(i++), subb.obj());
break;
}
case Object: {
BSONObjBuilder subb;
BSONObjIterator jt(e.embeddedObject());
while (jt.more()) {
append(subb , jt.next());
}
b.append(b.numStr(i++), subb.obj());
break;
}
default:
if (_include)
b.appendAs(e, b.numStr(i++));
}
}
}
void Projection::append( BSONObjBuilder& b , const BSONElement& e, const MatchDetails* details,
const ArrayOpType arrayOpType ) const {
FieldMap::const_iterator field = _fields.find( e.fieldName() );
if (field == _fields.end()) {
if (_include)
b.append(e);
}
else {
Projection& subfm = *field->second;
if ( ( subfm._fields.empty() && !subfm._special ) ||
!(e.type()==Object || e.type()==Array) ) {
// field map empty, or element is not an array/object
if (subfm._include)
b.append(e);
}
else if (e.type() == Object) {
BSONObjBuilder subb;
BSONObjIterator it(e.embeddedObject());
while (it.more()) {
subfm.append(subb, it.next(), details, arrayOpType);
}
b.append(e.fieldName(), subb.obj());
}
else { //Array
BSONObjBuilder matchedBuilder;
if ( details && arrayOpType == ARRAY_OP_POSITIONAL ) {
// $ positional operator specified
LOG(4) << "projection: checking if element " << e << " matched spec: "
<< getSpec() << " match details: " << *details << endl;
uassert( 16352, mongoutils::str::stream() << "positional operator ("
<< e.fieldName()
<< ".$) requires corresponding field in query specifier",
details && details->hasElemMatchKey() );
uassert( 16353, "positional operator element mismatch",
! e.embeddedObject()[details->elemMatchKey()].eoo() );
// append as the first and only element in the projected array
matchedBuilder.appendAs( e.embeddedObject()[details->elemMatchKey()], "0" );
}
else {
// append exact array; no subarray matcher specified
subfm.appendArray( matchedBuilder, e.embeddedObject() );
}
b.appendArray( e.fieldName(), matchedBuilder.obj() );
}
}
}
Projection::ArrayOpType Projection::getArrayOpType( ) const {
return _arrayOpType;
}
void Projection::validateQuery( const BSONObj query ) const {
// this function only validates positional operator ($) projections
if ( getArrayOpType() != ARRAY_OP_POSITIONAL )
return;
BSONObjIterator querySpecIter( query );
while ( querySpecIter.more() ) {
// for each query element
BSONElement queryElement = querySpecIter.next();
if ( mongoutils::str::equals( queryElement.fieldName(), "$and" ) )
// don't check $and to avoid deep comparison of the arguments.
// TODO: can be replaced with Matcher::FieldSink when complete (SERVER-4644)
return;
BSONObjIterator projectionSpecIter( getSpec() );
while ( projectionSpecIter.more() ) {
// for each projection element
BSONElement projectionElement = projectionSpecIter.next();
if ( mongoutils::str::contains( projectionElement.fieldName(), ".$" ) &&
mongoutils::str::before( queryElement.fieldName(), '.' ) ==
mongoutils::str::before( projectionElement.fieldName(), "." ) ) {
// found query spec that matches positional array projection spec
LOG(4) << "Query specifies field named for positional operator: "
<< queryElement.fieldName() << endl;
return;
}
}
}
uasserted( 16354, "Positional operator does not match the query specifier." );
}
Projection::KeyOnly* Projection::checkKey( const BSONObj& keyPattern ) const {
if ( _include ) {
// if we default to including then we can't
// use an index because we don't know what we're missing
return 0;
}
if ( _hasNonSimple )
return 0;
if ( _includeID && keyPattern["_id"].eoo() )
return 0;
// at this point we know its all { x : 1 } style
auto_ptr p( new KeyOnly() );
int got = 0;
BSONObjIterator i( keyPattern );
while ( i.more() ) {
BSONElement k = i.next();
if ( _source[k.fieldName()].type() ) {
if ( strchr( k.fieldName() , '.' ) ) {
// TODO we currently don't support dotted fields
// SERVER-2104
return 0;
}
if ( ! _includeID && mongoutils::str::equals( k.fieldName() , "_id" ) ) {
p->addNo();
}
else {
p->addYes( k.fieldName() );
got++;
}
}
else if ( mongoutils::str::equals( "_id" , k.fieldName() ) && _includeID ) {
p->addYes( "_id" );
}
else {
p->addNo();
}
}
int need = _source.nFields();
if ( ! _includeID )
need--;
if ( got == need )
return p.release();
return 0;
}
BSONObj Projection::KeyOnly::hydrate( const BSONObj& key ) const {
verify( _include.size() == _names.size() );
BSONObjBuilder b( key.objsize() + _stringSize + 16 );
BSONObjIterator i(key);
unsigned n=0;
while ( i.more() ) {
verify( n < _include.size() );
BSONElement e = i.next();
if ( _include[n] ) {
b.appendAs( e , _names[n] );
}
n++;
}
return b.obj();
}
}